@samitouri / QOS-React-2 / commits / 8bff8987e5

Don't bailout after Suspending in Legacy Mode (#19216)

* Add a failing test for legacy Suspense blocking context updates in memo * Add more test case coverage for variations of #17356 * Don't bailout after Suspending in Legacy Mode Co-authored-by: Tharuka Devendra <tsdevendra1@gmail.com>

Dan Abramov committed Jun 30, 2020 at 22:06 UTC 8bff8987e513486bb96018b80d7edb02d095ed06
6 files changed +507 -29
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+19 -5
@@ -63,6 +63,7 @@ import {
63 Update,
64 Ref,
65 Deletion,
66 + ForceUpdateForLegacySuspense,
67 } from './ReactSideEffectTags';
68 import ReactSharedInternals from 'shared/ReactSharedInternals';
69 import {
@@ -548,6 +549,13 @@ function updateSimpleMemoComponent(
549 workInProgress,
550 renderLanes,
551 );
552 + } else if (
553 + (current.effectTag & ForceUpdateForLegacySuspense) !==
554 + NoEffect
555 + ) {
556 + // This is a special case that only exists for legacy mode.
557 + // See https://github.com/facebook/react/pull/19216.
558 + didReceiveUpdate = true;
559 }
560 }
561 }
@@ -3263,11 +3271,17 @@ function beginWork(
3271 }
3272 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
3273 } else {
3266 - // An update was scheduled on this fiber, but there are no new props
3267 - // nor legacy context. Set this to false. If an update queue or context
3268 - // consumer produces a changed value, it will set this to true. Otherwise,
3269 - // the component will assume the children have not changed and bail out.
3270 - didReceiveUpdate = false;
3274 + if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
3275 + // This is a special case that only exists for legacy mode.
3276 + // See https://github.com/facebook/react/pull/19216.
3277 + didReceiveUpdate = true;
3278 + } else {
3279 + // An update was scheduled on this fiber, but there are no new props
3280 + // nor legacy context. Set this to false. If an update queue or context
3281 + // consumer produces a changed value, it will set this to true. Otherwise,
3282 + // the component will assume the children have not changed and bail out.
3283 + didReceiveUpdate = false;
3284 + }
3285 }
3286 } else {
3287 didReceiveUpdate = false;
packages/react-reconciler/src/ReactFiberBeginWork.old.js
+19 -5
@@ -63,6 +63,7 @@ import {
63 Update,
64 Ref,
65 Deletion,
66 + ForceUpdateForLegacySuspense,
67 } from './ReactSideEffectTags';
68 import ReactSharedInternals from 'shared/ReactSharedInternals';
69 import {
@@ -548,6 +549,13 @@ function updateSimpleMemoComponent(
549 workInProgress,
550 renderLanes,
551 );
552 + } else if (
553 + (current.effectTag & ForceUpdateForLegacySuspense) !==
554 + NoEffect
555 + ) {
556 + // This is a special case that only exists for legacy mode.
557 + // See https://github.com/facebook/react/pull/19216.
558 + didReceiveUpdate = true;
559 }
560 }
561 }
@@ -3263,11 +3271,17 @@ function beginWork(
3271 }
3272 return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
3273 } else {
3266 - // An update was scheduled on this fiber, but there are no new props
3267 - // nor legacy context. Set this to false. If an update queue or context
3268 - // consumer produces a changed value, it will set this to true. Otherwise,
3269 - // the component will assume the children have not changed and bail out.
3270 - didReceiveUpdate = false;
3274 + if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
3275 + // This is a special case that only exists for legacy mode.
3276 + // See https://github.com/facebook/react/pull/19216.
3277 + didReceiveUpdate = true;
3278 + } else {
3279 + // An update was scheduled on this fiber, but there are no new props
3280 + // nor legacy context. Set this to false. If an update queue or context
3281 + // consumer produces a changed value, it will set this to true. Otherwise,
3282 + // the component will assume the children have not changed and bail out.
3283 + didReceiveUpdate = false;
3284 + }
3285 }
3286 } else {
3287 didReceiveUpdate = false;
packages/react-reconciler/src/ReactFiberThrow.new.js
+2
@@ -28,6 +28,7 @@ import {
28 NoEffect,
29 ShouldCapture,
30 LifecycleEffectMask,
31 + ForceUpdateForLegacySuspense,
32 } from './ReactSideEffectTags';
33 import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
34 import {NoMode, BlockingMode} from './ReactTypeOfMode';
@@ -238,6 +239,7 @@ function throwException(
239 // should *not* suspend the commit.
240 if ((workInProgress.mode & BlockingMode) === NoMode) {
241 workInProgress.effectTag |= DidCapture;
242 + sourceFiber.effectTag |= ForceUpdateForLegacySuspense;
243
244 // We're going to commit this fiber even though it didn't complete.
245 // But we shouldn't call any lifecycle methods or callbacks. Remove
packages/react-reconciler/src/ReactFiberThrow.old.js
+2
@@ -28,6 +28,7 @@ import {
28 NoEffect,
29 ShouldCapture,
30 LifecycleEffectMask,
31 + ForceUpdateForLegacySuspense,
32 } from './ReactSideEffectTags';
33 import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
34 import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
@@ -249,6 +250,7 @@ function throwException(
250 // should *not* suspend the commit.
251 if ((workInProgress.mode & BlockingMode) === NoMode) {
252 workInProgress.effectTag |= DidCapture;
253 + sourceFiber.effectTag |= ForceUpdateForLegacySuspense;
254
255 // We're going to commit this fiber even though it didn't complete.
256 // But we shouldn't call any lifecycle methods or callbacks. Remove
packages/react-reconciler/src/ReactSideEffectTags.js
+21 -19
@@ -10,29 +10,31 @@
10 export type SideEffectTag = number;
11
12 // Don't change these two values. They're used by React Dev Tools.
13 -export const NoEffect = /* */ 0b00000000000000;
14 -export const PerformedWork = /* */ 0b00000000000001;
13 +export const NoEffect = /* */ 0b000000000000000;
14 +export const PerformedWork = /* */ 0b000000000000001;
15
16 // You can change the rest (and add more).
17 -export const Placement = /* */ 0b00000000000010;
18 -export const Update = /* */ 0b00000000000100;
19 -export const PlacementAndUpdate = /* */ 0b00000000000110;
20 -export const Deletion = /* */ 0b00000000001000;
21 -export const ContentReset = /* */ 0b00000000010000;
22 -export const Callback = /* */ 0b00000000100000;
23 -export const DidCapture = /* */ 0b00000001000000;
24 -export const Ref = /* */ 0b00000010000000;
25 -export const Snapshot = /* */ 0b00000100000000;
26 -export const Passive = /* */ 0b00001000000000;
27 -export const PassiveUnmountPendingDev = /* */ 0b10000000000000;
28 -export const Hydrating = /* */ 0b00010000000000;
29 -export const HydratingAndUpdate = /* */ 0b00010000000100;
17 +export const Placement = /* */ 0b000000000000010;
18 +export const Update = /* */ 0b000000000000100;
19 +export const PlacementAndUpdate = /* */ 0b000000000000110;
20 +export const Deletion = /* */ 0b000000000001000;
21 +export const ContentReset = /* */ 0b000000000010000;
22 +export const Callback = /* */ 0b000000000100000;
23 +export const DidCapture = /* */ 0b000000001000000;
24 +export const Ref = /* */ 0b000000010000000;
25 +export const Snapshot = /* */ 0b000000100000000;
26 +export const Passive = /* */ 0b000001000000000;
27 +export const PassiveUnmountPendingDev = /* */ 0b010000000000000;
28 +export const Hydrating = /* */ 0b000010000000000;
29 +export const HydratingAndUpdate = /* */ 0b000010000000100;
30
31 // Passive & Update & Callback & Ref & Snapshot
32 -export const LifecycleEffectMask = /* */ 0b00001110100100;
32 +export const LifecycleEffectMask = /* */ 0b000001110100100;
33
34 // Union of all host effects
35 -export const HostEffectMask = /* */ 0b00011111111111;
35 +export const HostEffectMask = /* */ 0b000011111111111;
36
37 -export const Incomplete = /* */ 0b00100000000000;
38 -export const ShouldCapture = /* */ 0b01000000000000;
37 +// These are not really side effects, but we still reuse this field.
38 +export const Incomplete = /* */ 0b000100000000000;
39 +export const ShouldCapture = /* */ 0b001000000000000;
40 +export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000;
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+444
@@ -628,6 +628,233 @@ describe('ReactSuspense', () => {
628 expect(Scheduler).toHaveYielded(['Suspend! [Hi]', 'Suspend! [Hi]']);
629 });
630
631 + it('updates memoized child of suspense component when context updates (simple memo)', () => {
632 + const {useContext, createContext, useState, memo} = React;
633 +
634 + const ValueContext = createContext(null);
635 +
636 + const MemoizedChild = memo(function MemoizedChild() {
637 + const text = useContext(ValueContext);
638 + try {
639 + TextResource.read([text, 1000]);
640 + Scheduler.unstable_yieldValue(text);
641 + return text;
642 + } catch (promise) {
643 + if (typeof promise.then === 'function') {
644 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
645 + } else {
646 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
647 + }
648 + throw promise;
649 + }
650 + });
651 +
652 + let setValue;
653 + function App() {
654 + const [value, _setValue] = useState('default');
655 + setValue = _setValue;
656 +
657 + return (
658 + <ValueContext.Provider value={value}>
659 + <Suspense fallback={<Text text="Loading..." />}>
660 + <MemoizedChild />
661 + </Suspense>
662 + </ValueContext.Provider>
663 + );
664 + }
665 +
666 + const root = ReactTestRenderer.create(<App />, {
667 + unstable_isConcurrent: true,
668 + });
669 + expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
670 + jest.advanceTimersByTime(1000);
671 +
672 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
673 + expect(Scheduler).toFlushAndYield(['default']);
674 + expect(root).toMatchRenderedOutput('default');
675 +
676 + act(() => setValue('new value'));
677 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
678 + jest.advanceTimersByTime(1000);
679 +
680 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
681 + expect(Scheduler).toFlushAndYield(['new value']);
682 + expect(root).toMatchRenderedOutput('new value');
683 + });
684 +
685 + it('updates memoized child of suspense component when context updates (manual memo)', () => {
686 + const {useContext, createContext, useState, memo} = React;
687 +
688 + const ValueContext = createContext(null);
689 +
690 + const MemoizedChild = memo(
691 + function MemoizedChild() {
692 + const text = useContext(ValueContext);
693 + try {
694 + TextResource.read([text, 1000]);
695 + Scheduler.unstable_yieldValue(text);
696 + return text;
697 + } catch (promise) {
698 + if (typeof promise.then === 'function') {
699 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
700 + } else {
701 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
702 + }
703 + throw promise;
704 + }
705 + },
706 + function areEqual(prevProps, nextProps) {
707 + return true;
708 + },
709 + );
710 +
711 + let setValue;
712 + function App() {
713 + const [value, _setValue] = useState('default');
714 + setValue = _setValue;
715 +
716 + return (
717 + <ValueContext.Provider value={value}>
718 + <Suspense fallback={<Text text="Loading..." />}>
719 + <MemoizedChild />
720 + </Suspense>
721 + </ValueContext.Provider>
722 + );
723 + }
724 +
725 + const root = ReactTestRenderer.create(<App />, {
726 + unstable_isConcurrent: true,
727 + });
728 + expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
729 + jest.advanceTimersByTime(1000);
730 +
731 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
732 + expect(Scheduler).toFlushAndYield(['default']);
733 + expect(root).toMatchRenderedOutput('default');
734 +
735 + act(() => setValue('new value'));
736 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
737 + jest.advanceTimersByTime(1000);
738 +
739 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
740 + expect(Scheduler).toFlushAndYield(['new value']);
741 + expect(root).toMatchRenderedOutput('new value');
742 + });
743 +
744 + it('updates memoized child of suspense component when context updates (function)', () => {
745 + const {useContext, createContext, useState} = React;
746 +
747 + const ValueContext = createContext(null);
748 +
749 + function MemoizedChild() {
750 + const text = useContext(ValueContext);
751 + try {
752 + TextResource.read([text, 1000]);
753 + Scheduler.unstable_yieldValue(text);
754 + return text;
755 + } catch (promise) {
756 + if (typeof promise.then === 'function') {
757 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
758 + } else {
759 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
760 + }
761 + throw promise;
762 + }
763 + }
764 +
765 + let setValue;
766 + function App({children}) {
767 + const [value, _setValue] = useState('default');
768 + setValue = _setValue;
769 +
770 + return (
771 + <ValueContext.Provider value={value}>{children}</ValueContext.Provider>
772 + );
773 + }
774 +
775 + const root = ReactTestRenderer.create(
776 + <App>
777 + <Suspense fallback={<Text text="Loading..." />}>
778 + <MemoizedChild />
779 + </Suspense>
780 + </App>,
781 + {
782 + unstable_isConcurrent: true,
783 + },
784 + );
785 + expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
786 + jest.advanceTimersByTime(1000);
787 +
788 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
789 + expect(Scheduler).toFlushAndYield(['default']);
790 + expect(root).toMatchRenderedOutput('default');
791 +
792 + act(() => setValue('new value'));
793 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
794 + jest.advanceTimersByTime(1000);
795 +
796 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
797 + expect(Scheduler).toFlushAndYield(['new value']);
798 + expect(root).toMatchRenderedOutput('new value');
799 + });
800 +
801 + it('updates memoized child of suspense component when context updates (forwardRef)', () => {
802 + const {forwardRef, useContext, createContext, useState} = React;
803 +
804 + const ValueContext = createContext(null);
805 +
806 + const MemoizedChild = forwardRef(() => {
807 + const text = useContext(ValueContext);
808 + try {
809 + TextResource.read([text, 1000]);
810 + Scheduler.unstable_yieldValue(text);
811 + return text;
812 + } catch (promise) {
813 + if (typeof promise.then === 'function') {
814 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
815 + } else {
816 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
817 + }
818 + throw promise;
819 + }
820 + });
821 +
822 + let setValue;
823 + function App({children}) {
824 + const [value, _setValue] = useState('default');
825 + setValue = _setValue;
826 +
827 + return (
828 + <ValueContext.Provider value={value}>{children}</ValueContext.Provider>
829 + );
830 + }
831 +
832 + const root = ReactTestRenderer.create(
833 + <App>
834 + <Suspense fallback={<Text text="Loading..." />}>
835 + <MemoizedChild />
836 + </Suspense>
837 + </App>,
838 + {
839 + unstable_isConcurrent: true,
840 + },
841 + );
842 + expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
843 + jest.advanceTimersByTime(1000);
844 +
845 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
846 + expect(Scheduler).toFlushAndYield(['default']);
847 + expect(root).toMatchRenderedOutput('default');
848 +
849 + act(() => setValue('new value'));
850 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
851 + jest.advanceTimersByTime(1000);
852 +
853 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
854 + expect(Scheduler).toFlushAndYield(['new value']);
855 + expect(root).toMatchRenderedOutput('new value');
856 + });
857 +
858 describe('outside concurrent mode', () => {
859 it('a mounted class component can suspend without losing state', () => {
860 class TextWithLifecycle extends React.Component {
@@ -1336,5 +1563,222 @@ describe('ReactSuspense', () => {
1563 root.update(<App name="world" />);
1564 jest.advanceTimersByTime(1000);
1565 });
1566 +
1567 + it('updates memoized child of suspense component when context updates (simple memo)', () => {
1568 + const {useContext, createContext, useState, memo} = React;
1569 +
1570 + const ValueContext = createContext(null);
1571 +
1572 + const MemoizedChild = memo(function MemoizedChild() {
1573 + const text = useContext(ValueContext);
1574 + try {
1575 + TextResource.read([text, 1000]);
1576 + Scheduler.unstable_yieldValue(text);
1577 + return text;
1578 + } catch (promise) {
1579 + if (typeof promise.then === 'function') {
1580 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1581 + } else {
1582 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
1583 + }
1584 + throw promise;
1585 + }
1586 + });
1587 +
1588 + let setValue;
1589 + function App() {
1590 + const [value, _setValue] = useState('default');
1591 + setValue = _setValue;
1592 +
1593 + return (
1594 + <ValueContext.Provider value={value}>
1595 + <Suspense fallback={<Text text="Loading..." />}>
1596 + <MemoizedChild />
1597 + </Suspense>
1598 + </ValueContext.Provider>
1599 + );
1600 + }
1601 +
1602 + const root = ReactTestRenderer.create(<App />);
1603 + expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
1604 + jest.advanceTimersByTime(1000);
1605 +
1606 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
1607 + expect(Scheduler).toFlushExpired(['default']);
1608 + expect(root).toMatchRenderedOutput('default');
1609 +
1610 + act(() => setValue('new value'));
1611 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
1612 + jest.advanceTimersByTime(1000);
1613 +
1614 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
1615 + expect(Scheduler).toFlushExpired(['new value']);
1616 + expect(root).toMatchRenderedOutput('new value');
1617 + });
1618 +
1619 + it('updates memoized child of suspense component when context updates (manual memo)', () => {
1620 + const {useContext, createContext, useState, memo} = React;
1621 +
1622 + const ValueContext = createContext(null);
1623 +
1624 + const MemoizedChild = memo(
1625 + function MemoizedChild() {
1626 + const text = useContext(ValueContext);
1627 + try {
1628 + TextResource.read([text, 1000]);
1629 + Scheduler.unstable_yieldValue(text);
1630 + return text;
1631 + } catch (promise) {
1632 + if (typeof promise.then === 'function') {
1633 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1634 + } else {
1635 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
1636 + }
1637 + throw promise;
1638 + }
1639 + },
1640 + function areEqual(prevProps, nextProps) {
1641 + return true;
1642 + },
1643 + );
1644 +
1645 + let setValue;
1646 + function App() {
1647 + const [value, _setValue] = useState('default');
1648 + setValue = _setValue;
1649 +
1650 + return (
1651 + <ValueContext.Provider value={value}>
1652 + <Suspense fallback={<Text text="Loading..." />}>
1653 + <MemoizedChild />
1654 + </Suspense>
1655 + </ValueContext.Provider>
1656 + );
1657 + }
1658 +
1659 + const root = ReactTestRenderer.create(<App />);
1660 + expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
1661 + jest.advanceTimersByTime(1000);
1662 +
1663 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
1664 + expect(Scheduler).toFlushExpired(['default']);
1665 + expect(root).toMatchRenderedOutput('default');
1666 +
1667 + act(() => setValue('new value'));
1668 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
1669 + jest.advanceTimersByTime(1000);
1670 +
1671 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
1672 + expect(Scheduler).toFlushExpired(['new value']);
1673 + expect(root).toMatchRenderedOutput('new value');
1674 + });
1675 +
1676 + it('updates memoized child of suspense component when context updates (function)', () => {
1677 + const {useContext, createContext, useState} = React;
1678 +
1679 + const ValueContext = createContext(null);
1680 +
1681 + function MemoizedChild() {
1682 + const text = useContext(ValueContext);
1683 + try {
1684 + TextResource.read([text, 1000]);
1685 + Scheduler.unstable_yieldValue(text);
1686 + return text;
1687 + } catch (promise) {
1688 + if (typeof promise.then === 'function') {
1689 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1690 + } else {
1691 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
1692 + }
1693 + throw promise;
1694 + }
1695 + }
1696 +
1697 + let setValue;
1698 + function App({children}) {
1699 + const [value, _setValue] = useState('default');
1700 + setValue = _setValue;
1701 +
1702 + return (
1703 + <ValueContext.Provider value={value}>
1704 + {children}
1705 + </ValueContext.Provider>
1706 + );
1707 + }
1708 +
1709 + const root = ReactTestRenderer.create(
1710 + <App>
1711 + <Suspense fallback={<Text text="Loading..." />}>
1712 + <MemoizedChild />
1713 + </Suspense>
1714 + </App>,
1715 + );
1716 + expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
1717 + jest.advanceTimersByTime(1000);
1718 +
1719 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
1720 + expect(Scheduler).toFlushExpired(['default']);
1721 + expect(root).toMatchRenderedOutput('default');
1722 +
1723 + act(() => setValue('new value'));
1724 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
1725 + jest.advanceTimersByTime(1000);
1726 +
1727 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
1728 + expect(Scheduler).toFlushExpired(['new value']);
1729 + expect(root).toMatchRenderedOutput('new value');
1730 + });
1731 +
1732 + it('updates memoized child of suspense component when context updates (forwardRef)', () => {
1733 + const {forwardRef, useContext, createContext, useState} = React;
1734 +
1735 + const ValueContext = createContext(null);
1736 +
1737 + const MemoizedChild = forwardRef(function MemoizedChild() {
1738 + const text = useContext(ValueContext);
1739 + try {
1740 + TextResource.read([text, 1000]);
1741 + Scheduler.unstable_yieldValue(text);
1742 + return text;
1743 + } catch (promise) {
1744 + if (typeof promise.then === 'function') {
1745 + Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
1746 + } else {
1747 + Scheduler.unstable_yieldValue(`Error! [${text}]`);
1748 + }
1749 + throw promise;
1750 + }
1751 + });
1752 +
1753 + let setValue;
1754 + function App() {
1755 + const [value, _setValue] = useState('default');
1756 + setValue = _setValue;
1757 +
1758 + return (
1759 + <ValueContext.Provider value={value}>
1760 + <Suspense fallback={<Text text="Loading..." />}>
1761 + <MemoizedChild />
1762 + </Suspense>
1763 + </ValueContext.Provider>
1764 + );
1765 + }
1766 +
1767 + const root = ReactTestRenderer.create(<App />);
1768 + expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
1769 + jest.advanceTimersByTime(1000);
1770 +
1771 + expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
1772 + expect(Scheduler).toFlushExpired(['default']);
1773 + expect(root).toMatchRenderedOutput('default');
1774 +
1775 + act(() => setValue('new value'));
1776 + expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
1777 + jest.advanceTimersByTime(1000);
1778 +
1779 + expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
1780 + expect(Scheduler).toFlushExpired(['new value']);
1781 + expect(root).toMatchRenderedOutput('new value');
1782 + });
1783 });
1784 });