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
});