@samitouri / QOS-React / commits / f523b2e0d3

Use fewer global variables in Hooks (#17480)

* We don't need the global state for this * Move componentUpdateQueue and sideEffectTag out of global state * Move firstWorkInProgressHook off global state * Move remainingExpirationTime off global state * Reset fiber to its current state if it throws * Move rerender error check to avoid global state This means that it's harder to find it since it's not in the dispatch function's stack but we can add a DEV only one for that if we really need it. Alternatively, we can check it in against the renderUpdates queue. * Move next___Hook out of global state * Assert that currentlyRenderingFiber is always set When accessed, this should always be set. This could enforced by storing this on the dispatcher for example. * Add another test just to be safe

Sebastian Markbåge committed Dec 3, 2019 at 12:51 UTC f523b2e0d369e3f42938b56784f9ce1990838753
3 files changed +262 -114
packages/react-reconciler/src/ReactFiberHooks.js
+91 -114
@@ -12,7 +12,6 @@ import type {
12 ReactContext,
13 ReactEventResponderListener,
14 } from 'shared/ReactTypes';
15 -import type {SideEffectTag} from 'shared/ReactSideEffectTags';
15 import type {Fiber} from './ReactFiber';
16 import type {ExpirationTime} from './ReactFiberExpirationTime';
17 import type {HookEffectTag} from './ReactHookEffectTags';
@@ -175,21 +174,14 @@ type Dispatch<A> = A => void;
174 let renderExpirationTime: ExpirationTime = NoWork;
175 // The work-in-progress fiber. I've named it differently to distinguish it from
176 // the work-in-progress hook.
178 -let currentlyRenderingFiber: Fiber | null = null;
177 +let currentlyRenderingFiber: Fiber = (null: any);
178
179 // Hooks are stored as a linked list on the fiber's memoizedState field. The
180 // current hook list is the list that belongs to the current fiber. The
181 // work-in-progress hook list is a new list that will be added to the
182 // work-in-progress fiber.
183 let currentHook: Hook | null = null;
185 -let nextCurrentHook: Hook | null = null;
186 -let firstWorkInProgressHook: Hook | null = null;
184 let workInProgressHook: Hook | null = null;
188 -let nextWorkInProgressHook: Hook | null = null;
189 -
190 -let remainingExpirationTime: ExpirationTime = NoWork;
191 -let componentUpdateQueue: FunctionComponentUpdateQueue | null = null;
192 -let sideEffectTag: SideEffectTag = 0;
185
186 // Updates scheduled during render will trigger an immediate re-render at the
187 // end of the current pass. We can't store these updates on the normal queue,
@@ -206,8 +198,7 @@ let renderPhaseUpdates: Map<
198 UpdateQueue<any, any>,
199 Update<any, any>,
200 > | null = null;
209 -// Counter to prevent infinite loops.
210 -let numberOfReRenders: number = 0;
201 +
202 const RE_RENDER_LIMIT = 25;
203
204 // In DEV, this is the name of the currently executing primitive hook
@@ -267,9 +258,7 @@ function checkDepsAreArrayDev(deps: mixed) {
258
259 function warnOnHookMismatchInDev(currentHookName: HookType) {
260 if (__DEV__) {
270 - const componentName = getComponentName(
271 - ((currentlyRenderingFiber: any): Fiber).type,
272 - );
261 + const componentName = getComponentName(currentlyRenderingFiber.type);
262 if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {
263 didWarnAboutMismatchedHooksForComponent.add(componentName);
264
@@ -386,7 +375,6 @@ export function renderWithHooks(
375 ): any {
376 renderExpirationTime = nextRenderExpirationTime;
377 currentlyRenderingFiber = workInProgress;
389 - nextCurrentHook = current !== null ? current.memoizedState : null;
378
379 if (__DEV__) {
380 hookTypesDev =
@@ -399,27 +387,26 @@ export function renderWithHooks(
387 current !== null && current.type !== workInProgress.type;
388 }
389
390 + workInProgress.memoizedState = null;
391 + workInProgress.updateQueue = null;
392 + workInProgress.expirationTime = NoWork;
393 +
394 // The following should have already been reset
395 // currentHook = null;
396 // workInProgressHook = null;
397
406 - // remainingExpirationTime = NoWork;
407 - // componentUpdateQueue = null;
408 -
398 // didScheduleRenderPhaseUpdate = false;
399 // renderPhaseUpdates = null;
411 - // numberOfReRenders = 0;
412 - // sideEffectTag = 0;
400
401 // TODO Warn if no hooks are used at all during mount, then some are used during update.
415 - // Currently we will identify the update render as a mount because nextCurrentHook === null.
402 + // Currently we will identify the update render as a mount because memoizedState === null.
403 // This is tricky because it's valid for certain types of components (e.g. React.lazy)
404
418 - // Using nextCurrentHook to differentiate between mount/update only works if at least one stateful hook is used.
405 + // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.
406 // Non-stateful hooks (e.g. context) don't get added to memoizedState,
420 - // so nextCurrentHook would be null during updates and mounts.
407 + // so memoizedState would be null during updates and mounts.
408 if (__DEV__) {
422 - if (nextCurrentHook !== null) {
409 + if (current !== null && current.memoizedState !== null) {
410 ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
411 } else if (hookTypesDev !== null) {
412 // This dispatcher handles an edge case where a component is updating,
@@ -433,7 +420,7 @@ export function renderWithHooks(
420 }
421 } else {
422 ReactCurrentDispatcher.current =
436 - nextCurrentHook === null
423 + current === null || current.memoizedState === null
424 ? HooksDispatcherOnMount
425 : HooksDispatcherOnUpdate;
426 }
@@ -441,8 +428,17 @@ export function renderWithHooks(
428 let children = Component(props, refOrContext);
429
430 if (didScheduleRenderPhaseUpdate) {
431 + // Counter to prevent infinite loops.
432 + let numberOfReRenders: number = 0;
433 do {
434 didScheduleRenderPhaseUpdate = false;
435 +
436 + invariant(
437 + numberOfReRenders < RE_RENDER_LIMIT,
438 + 'Too many re-renders. React limits the number of renders to prevent ' +
439 + 'an infinite loop.',
440 + );
441 +
442 numberOfReRenders += 1;
443 if (__DEV__) {
444 // Even when hot reloading, allow dependencies to stabilize
@@ -451,12 +447,10 @@ export function renderWithHooks(
447 }
448
449 // Start over from the beginning of the list
454 - nextCurrentHook = current !== null ? current.memoizedState : null;
455 - nextWorkInProgressHook = firstWorkInProgressHook;
456 -
450 currentHook = null;
451 workInProgressHook = null;
459 - componentUpdateQueue = null;
452 +
453 + workInProgress.updateQueue = null;
454
455 if (__DEV__) {
456 // Also validate hook order for cascading updates.
@@ -471,22 +465,14 @@ export function renderWithHooks(
465 } while (didScheduleRenderPhaseUpdate);
466
467 renderPhaseUpdates = null;
474 - numberOfReRenders = 0;
468 }
469
470 // We can assume the previous dispatcher is always this one, since we set it
471 // at the beginning of the render phase and there's no re-entrancy.
472 ReactCurrentDispatcher.current = ContextOnlyDispatcher;
473
481 - const renderedWork: Fiber = (currentlyRenderingFiber: any);
482 -
483 - renderedWork.memoizedState = firstWorkInProgressHook;
484 - renderedWork.expirationTime = remainingExpirationTime;
485 - renderedWork.updateQueue = (componentUpdateQueue: any);
486 - renderedWork.effectTag |= sideEffectTag;
487 -
474 if (__DEV__) {
489 - renderedWork._debugHookTypes = hookTypesDev;
475 + workInProgress._debugHookTypes = hookTypesDev;
476 }
477
478 // This check uses currentHook so that it works the same in DEV and prod bundles.
@@ -495,13 +481,10 @@ export function renderWithHooks(
481 currentHook !== null && currentHook.next !== null;
482
483 renderExpirationTime = NoWork;
498 - currentlyRenderingFiber = null;
484 + currentlyRenderingFiber = (null: any);
485
486 currentHook = null;
501 - nextCurrentHook = null;
502 - firstWorkInProgressHook = null;
487 workInProgressHook = null;
504 - nextWorkInProgressHook = null;
488
489 if (__DEV__) {
490 currentHookNameInDev = null;
@@ -509,14 +492,9 @@ export function renderWithHooks(
492 hookTypesUpdateIndexDev = -1;
493 }
494
512 - remainingExpirationTime = NoWork;
513 - componentUpdateQueue = null;
514 - sideEffectTag = 0;
515 -
495 // These were reset above
496 // didScheduleRenderPhaseUpdate = false;
497 // renderPhaseUpdates = null;
519 - // numberOfReRenders = 0;
498
499 invariant(
500 !didRenderTooFewHooks,
@@ -549,13 +527,10 @@ export function resetHooks(): void {
527 // component is a module-style component.
528
529 renderExpirationTime = NoWork;
552 - currentlyRenderingFiber = null;
530 + currentlyRenderingFiber = (null: any);
531
532 currentHook = null;
555 - nextCurrentHook = null;
556 - firstWorkInProgressHook = null;
533 workInProgressHook = null;
558 - nextWorkInProgressHook = null;
534
535 if (__DEV__) {
536 hookTypesDev = null;
@@ -564,13 +539,8 @@ export function resetHooks(): void {
539 currentHookNameInDev = null;
540 }
541
567 - remainingExpirationTime = NoWork;
568 - componentUpdateQueue = null;
569 - sideEffectTag = 0;
570 -
542 didScheduleRenderPhaseUpdate = false;
543 renderPhaseUpdates = null;
573 - numberOfReRenders = 0;
544 }
545
546 function mountWorkInProgressHook(): Hook {
@@ -586,7 +556,7 @@ function mountWorkInProgressHook(): Hook {
556
557 if (workInProgressHook === null) {
558 // This is the first hook in the list
589 - firstWorkInProgressHook = workInProgressHook = hook;
559 + currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
560 } else {
561 // Append to the end of the list
562 workInProgressHook = workInProgressHook.next = hook;
@@ -600,15 +570,34 @@ function updateWorkInProgressHook(): Hook {
570 // clone, or a work-in-progress hook from a previous render pass that we can
571 // use as a base. When we reach the end of the base list, we must switch to
572 // the dispatcher used for mounts.
573 + let nextCurrentHook: null | Hook;
574 + if (currentHook === null) {
575 + let current = currentlyRenderingFiber.alternate;
576 + if (current !== null) {
577 + nextCurrentHook = current.memoizedState;
578 + } else {
579 + nextCurrentHook = null;
580 + }
581 + } else {
582 + nextCurrentHook = currentHook.next;
583 + }
584 +
585 + let nextWorkInProgressHook: null | Hook;
586 + if (workInProgressHook === null) {
587 + nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
588 + } else {
589 + nextWorkInProgressHook = workInProgressHook.next;
590 + }
591 +
592 if (nextWorkInProgressHook !== null) {
593 // There's already a work-in-progress. Reuse it.
594 workInProgressHook = nextWorkInProgressHook;
595 nextWorkInProgressHook = workInProgressHook.next;
596
597 currentHook = nextCurrentHook;
609 - nextCurrentHook = currentHook !== null ? currentHook.next : null;
598 } else {
599 // Clone from the current hook.
600 +
601 invariant(
602 nextCurrentHook !== null,
603 'Rendered more hooks than during the previous render.',
@@ -627,12 +616,11 @@ function updateWorkInProgressHook(): Hook {
616
617 if (workInProgressHook === null) {
618 // This is the first hook in the list.
630 - workInProgressHook = firstWorkInProgressHook = newHook;
619 + currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
620 } else {
621 // Append to the end of the list.
622 workInProgressHook = workInProgressHook.next = newHook;
623 }
635 - nextCurrentHook = currentHook.next;
624 }
625 return workInProgressHook;
626 }
@@ -668,8 +656,7 @@ function mountReducer<S, I, A>(
656 });
657 const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
658 null,
671 - // Flow doesn't know this is non-null, but we do.
672 - ((currentlyRenderingFiber: any): Fiber),
659 + currentlyRenderingFiber,
660 queue,
661 ): any));
662 return [hook.memoizedState, dispatch];
@@ -689,45 +676,43 @@ function updateReducer<S, I, A>(
676
677 queue.lastRenderedReducer = reducer;
678
692 - if (numberOfReRenders > 0) {
679 + if (renderPhaseUpdates !== null) {
680 // This is a re-render. Apply the new render phase updates to the previous
681 // work-in-progress hook.
682 const dispatch: Dispatch<A> = (queue.dispatch: any);
696 - if (renderPhaseUpdates !== null) {
697 - // Render phase updates are stored in a map of queue -> linked list
698 - const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
699 - if (firstRenderPhaseUpdate !== undefined) {
700 - renderPhaseUpdates.delete(queue);
701 - let newState = hook.memoizedState;
702 - let update = firstRenderPhaseUpdate;
703 - do {
704 - // Process this render phase update. We don't have to check the
705 - // priority because it will always be the same as the current
706 - // render's.
707 - const action = update.action;
708 - newState = reducer(newState, action);
709 - update = update.next;
710 - } while (update !== null);
711 -
712 - // Mark that the fiber performed work, but only if the new state is
713 - // different from the current state.
714 - if (!is(newState, hook.memoizedState)) {
715 - markWorkInProgressReceivedUpdate();
716 - }
683 + // Render phase updates are stored in a map of queue -> linked list
684 + const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
685 + if (firstRenderPhaseUpdate !== undefined) {
686 + renderPhaseUpdates.delete(queue);
687 + let newState = hook.memoizedState;
688 + let update = firstRenderPhaseUpdate;
689 + do {
690 + // Process this render phase update. We don't have to check the
691 + // priority because it will always be the same as the current
692 + // render's.
693 + const action = update.action;
694 + newState = reducer(newState, action);
695 + update = update.next;
696 + } while (update !== null);
697 +
698 + // Mark that the fiber performed work, but only if the new state is
699 + // different from the current state.
700 + if (!is(newState, hook.memoizedState)) {
701 + markWorkInProgressReceivedUpdate();
702 + }
703
718 - hook.memoizedState = newState;
719 - // Don't persist the state accumulated from the render phase updates to
720 - // the base state unless the queue is empty.
721 - // TODO: Not sure if this is the desired semantics, but it's what we
722 - // do for gDSFP. I can't remember why.
723 - if (hook.baseUpdate === queue.last) {
724 - hook.baseState = newState;
725 - }
704 + hook.memoizedState = newState;
705 + // Don't persist the state accumulated from the render phase updates to
706 + // the base state unless the queue is empty.
707 + // TODO: Not sure if this is the desired semantics, but it's what we
708 + // do for gDSFP. I can't remember why.
709 + if (hook.baseUpdate === queue.last) {
710 + hook.baseState = newState;
711 + }
712
727 - queue.lastRenderedState = newState;
713 + queue.lastRenderedState = newState;
714
729 - return [newState, dispatch];
730 - }
715 + return [newState, dispatch];
716 }
717 return [hook.memoizedState, dispatch];
718 }
@@ -770,9 +755,9 @@ function updateReducer<S, I, A>(
755 newBaseState = newState;
756 }
757 // Update the remaining priority in the queue.
773 - if (updateExpirationTime > remainingExpirationTime) {
774 - remainingExpirationTime = updateExpirationTime;
775 - markUnprocessedUpdateTime(remainingExpirationTime);
758 + if (updateExpirationTime > currentlyRenderingFiber.expirationTime) {
759 + currentlyRenderingFiber.expirationTime = updateExpirationTime;
760 + markUnprocessedUpdateTime(updateExpirationTime);
761 }
762 } else {
763 // This update does have sufficient priority.
@@ -842,8 +827,7 @@ function mountState<S>(
827 BasicStateAction<S>,
828 > = (queue.dispatch = (dispatchAction.bind(
829 null,
845 - // Flow doesn't know this is non-null, but we do.
846 - ((currentlyRenderingFiber: any): Fiber),
830 + currentlyRenderingFiber,
831 queue,
832 ): any));
833 return [hook.memoizedState, dispatch];
@@ -864,8 +848,10 @@ function pushEffect(tag, create, destroy, deps) {
848 // Circular
849 next: (null: any),
850 };
851 + let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
852 if (componentUpdateQueue === null) {
853 componentUpdateQueue = createFunctionComponentUpdateQueue();
854 + currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
855 componentUpdateQueue.lastEffect = effect.next = effect;
856 } else {
857 const lastEffect = componentUpdateQueue.lastEffect;
@@ -899,7 +885,7 @@ function updateRef<T>(initialValue: T): {current: T} {
885 function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
886 const hook = mountWorkInProgressHook();
887 const nextDeps = deps === undefined ? null : deps;
902 - sideEffectTag |= fiberEffectTag;
888 + currentlyRenderingFiber.effectTag |= fiberEffectTag;
889 hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
890 }
891
@@ -920,7 +906,8 @@ function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void {
906 }
907 }
908
923 - sideEffectTag |= fiberEffectTag;
909 + currentlyRenderingFiber.effectTag |= fiberEffectTag;
910 +
911 hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
912 }
913
@@ -931,9 +918,7 @@ function mountEffect(
918 if (__DEV__) {
919 // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
920 if ('undefined' !== typeof jest) {
934 - warnIfNotCurrentlyActingEffectsInDEV(
935 - ((currentlyRenderingFiber: any): Fiber),
936 - );
921 + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
922 }
923 }
924 return mountEffectImpl(
@@ -951,9 +936,7 @@ function updateEffect(
936 if (__DEV__) {
937 // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
938 if ('undefined' !== typeof jest) {
954 - warnIfNotCurrentlyActingEffectsInDEV(
955 - ((currentlyRenderingFiber: any): Fiber),
956 - );
939 + warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber);
940 }
941 }
942 return updateEffectImpl(
@@ -1222,12 +1205,6 @@ function dispatchAction<S, A>(
1205 queue: UpdateQueue<S, A>,
1206 action: A,
1207 ) {
1225 - invariant(
1226 - numberOfReRenders < RE_RENDER_LIMIT,
1227 - 'Too many re-renders. React limits the number of renders to prevent ' +
1228 - 'an infinite loop.',
1229 - );
1230 -
1208 if (__DEV__) {
1209 warning(
1210 typeof arguments[3] !== 'function',
packages/react-reconciler/src/ReactFiberThrow.js
+12
@@ -195,6 +195,18 @@ function throwException(
195 // This is a thenable.
196 const thenable: Thenable = (value: any);
197
198 + if ((sourceFiber.mode & BlockingMode) === NoMode) {
199 + // Reset the memoizedState to what it was before we attempted
200 + // to render it.
201 + let currentSource = sourceFiber.alternate;
202 + if (currentSource) {
203 + sourceFiber.memoizedState = currentSource.memoizedState;
204 + sourceFiber.expirationTime = currentSource.expirationTime;
205 + } else {
206 + sourceFiber.memoizedState = null;
207 + }
208 + }
209 +
210 checkForWrongSuspensePriorityInDEV(sourceFiber);
211
212 let hasInvisibleParentBoundary = hasSuspenseContext(
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+159
@@ -1512,6 +1512,165 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1512 );
1513 });
1514
1515 + it('does not call lifecycles of a suspended component (hooks)', async () => {
1516 + function TextWithLifecycle(props) {
1517 + React.useLayoutEffect(
1518 + () => {
1519 + Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1520 + return () => {
1521 + Scheduler.unstable_yieldValue(
1522 + `Destroy Layout Effect [${props.text}]`,
1523 + );
1524 + };
1525 + },
1526 + [props.text],
1527 + );
1528 + React.useEffect(
1529 + () => {
1530 + Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1531 + return () => {
1532 + Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1533 + };
1534 + },
1535 + [props.text],
1536 + );
1537 + return <Text {...props} />;
1538 + }
1539 +
1540 + function AsyncTextWithLifecycle(props) {
1541 + React.useLayoutEffect(
1542 + () => {
1543 + Scheduler.unstable_yieldValue(`Layout Effect [${props.text}]`);
1544 + return () => {
1545 + Scheduler.unstable_yieldValue(
1546 + `Destroy Layout Effect [${props.text}]`,
1547 + );
1548 + };
1549 + },
1550 + [props.text],
1551 + );
1552 + React.useEffect(
1553 + () => {
1554 + Scheduler.unstable_yieldValue(`Effect [${props.text}]`);
1555 + return () => {
1556 + Scheduler.unstable_yieldValue(`Destroy Effect [${props.text}]`);
1557 + };
1558 + },
1559 + [props.text],
1560 + );
1561 + const text = props.text;
1562 + const ms = props.ms;
1563 + try {
1564 + TextResource.read([text, ms]);
1565 + Scheduler.unstable_yieldValue(text);
1566 + return <span prop={text} />;
1567 + } catch (promise) {
1568 + if (typeof promise.then === 'function') {
1569 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1570 + } else {
1571 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
1572 + }
1573 + throw promise;
1574 + }
1575 + }
1576 +
1577 + function App({text}) {
1578 + return (
1579 + <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
1580 + <TextWithLifecycle text="A" />
1581 + <AsyncTextWithLifecycle ms={100} text={text} />
1582 + <TextWithLifecycle text="C" />
1583 + </Suspense>
1584 + );
1585 + }
1586 +
1587 + ReactNoop.renderLegacySyncRoot(<App text="B" />, () =>
1588 + Scheduler.unstable_yieldValue('Commit root'),
1589 + );
1590 + expect(Scheduler).toHaveYielded([
1591 + 'A',
1592 + 'Suspend! [B]',
1593 + 'C',
1594 + 'Loading...',
1595 +
1596 + 'Layout Effect [A]',
1597 + // B's effect should not fire because it suspended
1598 + // 'Layout Effect [B]',
1599 + 'Layout Effect [C]',
1600 + 'Layout Effect [Loading...]',
1601 + 'Commit root',
1602 + ]);
1603 +
1604 + // Flush passive effects.
1605 + expect(Scheduler).toFlushAndYield([
1606 + 'Effect [A]',
1607 + // B's effect should not fire because it suspended
1608 + // 'Effect [B]',
1609 + 'Effect [C]',
1610 + 'Effect [Loading...]',
1611 + ]);
1612 +
1613 + expect(ReactNoop).toMatchRenderedOutput(
1614 + <>
1615 + <span hidden={true} prop="A" />
1616 + <span hidden={true} prop="C" />
1617 + <span prop="Loading..." />
1618 + </>,
1619 + );
1620 +
1621 + Scheduler.unstable_advanceTime(500);
1622 + await advanceTimers(500);
1623 +
1624 + expect(Scheduler).toHaveYielded(['Promise resolved [B]']);
1625 +
1626 + expect(Scheduler).toFlushAndYield([
1627 + 'B',
1628 + 'Destroy Layout Effect [Loading...]',
1629 + 'Destroy Effect [Loading...]',
1630 + 'Layout Effect [B]',
1631 + 'Effect [B]',
1632 + ]);
1633 +
1634 + // Update
1635 + ReactNoop.renderLegacySyncRoot(<App text="B2" />, () =>
1636 + Scheduler.unstable_yieldValue('Commit root'),
1637 + );
1638 +
1639 + expect(Scheduler).toHaveYielded([
1640 + 'A',
1641 + 'Suspend! [B2]',
1642 + 'C',
1643 + 'Loading...',
1644 +
1645 + // B2's effect should not fire because it suspended
1646 + // 'Layout Effect [B2]',
1647 + 'Layout Effect [Loading...]',
1648 + 'Commit root',
1649 + ]);
1650 +
1651 + // Flush passive effects.
1652 + expect(Scheduler).toFlushAndYield([
1653 + // B2's effect should not fire because it suspended
1654 + // 'Effect [B2]',
1655 + 'Effect [Loading...]',
1656 + ]);
1657 +
1658 + Scheduler.unstable_advanceTime(500);
1659 + await advanceTimers(500);
1660 +
1661 + expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
1662 +
1663 + expect(Scheduler).toFlushAndYield([
1664 + 'B2',
1665 + 'Destroy Layout Effect [Loading...]',
1666 + 'Destroy Effect [Loading...]',
1667 + 'Destroy Layout Effect [B]',
1668 + 'Layout Effect [B2]',
1669 + 'Destroy Effect [B]',
1670 + 'Effect [B2]',
1671 + ]);
1672 + });
1673 +
1674 it('suspends for longer if something took a long (CPU bound) time to render', async () => {
1675 function Foo({renderContent}) {
1676 Scheduler.unstable_yieldValue('Foo');