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>
This commit is contained in:
parent
f4097c1aef
commit
8bff8987e5
|
@ -63,6 +63,7 @@ import {
|
|||
Update,
|
||||
Ref,
|
||||
Deletion,
|
||||
ForceUpdateForLegacySuspense,
|
||||
} from './ReactSideEffectTags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
|
@ -548,6 +549,13 @@ function updateSimpleMemoComponent(
|
|||
workInProgress,
|
||||
renderLanes,
|
||||
);
|
||||
} else if (
|
||||
(current.effectTag & ForceUpdateForLegacySuspense) !==
|
||||
NoEffect
|
||||
) {
|
||||
// This is a special case that only exists for legacy mode.
|
||||
// See https://github.com/facebook/react/pull/19216.
|
||||
didReceiveUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3263,11 +3271,17 @@ function beginWork(
|
|||
}
|
||||
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
|
||||
} else {
|
||||
// An update was scheduled on this fiber, but there are no new props
|
||||
// nor legacy context. Set this to false. If an update queue or context
|
||||
// consumer produces a changed value, it will set this to true. Otherwise,
|
||||
// the component will assume the children have not changed and bail out.
|
||||
didReceiveUpdate = false;
|
||||
if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
|
||||
// This is a special case that only exists for legacy mode.
|
||||
// See https://github.com/facebook/react/pull/19216.
|
||||
didReceiveUpdate = true;
|
||||
} else {
|
||||
// An update was scheduled on this fiber, but there are no new props
|
||||
// nor legacy context. Set this to false. If an update queue or context
|
||||
// consumer produces a changed value, it will set this to true. Otherwise,
|
||||
// the component will assume the children have not changed and bail out.
|
||||
didReceiveUpdate = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
didReceiveUpdate = false;
|
||||
|
|
|
@ -63,6 +63,7 @@ import {
|
|||
Update,
|
||||
Ref,
|
||||
Deletion,
|
||||
ForceUpdateForLegacySuspense,
|
||||
} from './ReactSideEffectTags';
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {
|
||||
|
@ -548,6 +549,13 @@ function updateSimpleMemoComponent(
|
|||
workInProgress,
|
||||
renderLanes,
|
||||
);
|
||||
} else if (
|
||||
(current.effectTag & ForceUpdateForLegacySuspense) !==
|
||||
NoEffect
|
||||
) {
|
||||
// This is a special case that only exists for legacy mode.
|
||||
// See https://github.com/facebook/react/pull/19216.
|
||||
didReceiveUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3263,11 +3271,17 @@ function beginWork(
|
|||
}
|
||||
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
|
||||
} else {
|
||||
// An update was scheduled on this fiber, but there are no new props
|
||||
// nor legacy context. Set this to false. If an update queue or context
|
||||
// consumer produces a changed value, it will set this to true. Otherwise,
|
||||
// the component will assume the children have not changed and bail out.
|
||||
didReceiveUpdate = false;
|
||||
if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
|
||||
// This is a special case that only exists for legacy mode.
|
||||
// See https://github.com/facebook/react/pull/19216.
|
||||
didReceiveUpdate = true;
|
||||
} else {
|
||||
// An update was scheduled on this fiber, but there are no new props
|
||||
// nor legacy context. Set this to false. If an update queue or context
|
||||
// consumer produces a changed value, it will set this to true. Otherwise,
|
||||
// the component will assume the children have not changed and bail out.
|
||||
didReceiveUpdate = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
didReceiveUpdate = false;
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
NoEffect,
|
||||
ShouldCapture,
|
||||
LifecycleEffectMask,
|
||||
ForceUpdateForLegacySuspense,
|
||||
} from './ReactSideEffectTags';
|
||||
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.new';
|
||||
import {NoMode, BlockingMode} from './ReactTypeOfMode';
|
||||
|
@ -238,6 +239,7 @@ function throwException(
|
|||
// should *not* suspend the commit.
|
||||
if ((workInProgress.mode & BlockingMode) === NoMode) {
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
sourceFiber.effectTag |= ForceUpdateForLegacySuspense;
|
||||
|
||||
// We're going to commit this fiber even though it didn't complete.
|
||||
// But we shouldn't call any lifecycle methods or callbacks. Remove
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
NoEffect,
|
||||
ShouldCapture,
|
||||
LifecycleEffectMask,
|
||||
ForceUpdateForLegacySuspense,
|
||||
} from './ReactSideEffectTags';
|
||||
import {shouldCaptureSuspense} from './ReactFiberSuspenseComponent.old';
|
||||
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
|
||||
|
@ -249,6 +250,7 @@ function throwException(
|
|||
// should *not* suspend the commit.
|
||||
if ((workInProgress.mode & BlockingMode) === NoMode) {
|
||||
workInProgress.effectTag |= DidCapture;
|
||||
sourceFiber.effectTag |= ForceUpdateForLegacySuspense;
|
||||
|
||||
// We're going to commit this fiber even though it didn't complete.
|
||||
// But we shouldn't call any lifecycle methods or callbacks. Remove
|
||||
|
|
|
@ -10,29 +10,31 @@
|
|||
export type SideEffectTag = number;
|
||||
|
||||
// Don't change these two values. They're used by React Dev Tools.
|
||||
export const NoEffect = /* */ 0b00000000000000;
|
||||
export const PerformedWork = /* */ 0b00000000000001;
|
||||
export const NoEffect = /* */ 0b000000000000000;
|
||||
export const PerformedWork = /* */ 0b000000000000001;
|
||||
|
||||
// You can change the rest (and add more).
|
||||
export const Placement = /* */ 0b00000000000010;
|
||||
export const Update = /* */ 0b00000000000100;
|
||||
export const PlacementAndUpdate = /* */ 0b00000000000110;
|
||||
export const Deletion = /* */ 0b00000000001000;
|
||||
export const ContentReset = /* */ 0b00000000010000;
|
||||
export const Callback = /* */ 0b00000000100000;
|
||||
export const DidCapture = /* */ 0b00000001000000;
|
||||
export const Ref = /* */ 0b00000010000000;
|
||||
export const Snapshot = /* */ 0b00000100000000;
|
||||
export const Passive = /* */ 0b00001000000000;
|
||||
export const PassiveUnmountPendingDev = /* */ 0b10000000000000;
|
||||
export const Hydrating = /* */ 0b00010000000000;
|
||||
export const HydratingAndUpdate = /* */ 0b00010000000100;
|
||||
export const Placement = /* */ 0b000000000000010;
|
||||
export const Update = /* */ 0b000000000000100;
|
||||
export const PlacementAndUpdate = /* */ 0b000000000000110;
|
||||
export const Deletion = /* */ 0b000000000001000;
|
||||
export const ContentReset = /* */ 0b000000000010000;
|
||||
export const Callback = /* */ 0b000000000100000;
|
||||
export const DidCapture = /* */ 0b000000001000000;
|
||||
export const Ref = /* */ 0b000000010000000;
|
||||
export const Snapshot = /* */ 0b000000100000000;
|
||||
export const Passive = /* */ 0b000001000000000;
|
||||
export const PassiveUnmountPendingDev = /* */ 0b010000000000000;
|
||||
export const Hydrating = /* */ 0b000010000000000;
|
||||
export const HydratingAndUpdate = /* */ 0b000010000000100;
|
||||
|
||||
// Passive & Update & Callback & Ref & Snapshot
|
||||
export const LifecycleEffectMask = /* */ 0b00001110100100;
|
||||
export const LifecycleEffectMask = /* */ 0b000001110100100;
|
||||
|
||||
// Union of all host effects
|
||||
export const HostEffectMask = /* */ 0b00011111111111;
|
||||
export const HostEffectMask = /* */ 0b000011111111111;
|
||||
|
||||
export const Incomplete = /* */ 0b00100000000000;
|
||||
export const ShouldCapture = /* */ 0b01000000000000;
|
||||
// These are not really side effects, but we still reuse this field.
|
||||
export const Incomplete = /* */ 0b000100000000000;
|
||||
export const ShouldCapture = /* */ 0b001000000000000;
|
||||
export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000;
|
||||
|
|
|
@ -628,6 +628,233 @@ describe('ReactSuspense', () => {
|
|||
expect(Scheduler).toHaveYielded(['Suspend! [Hi]', 'Suspend! [Hi]']);
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (simple memo)', () => {
|
||||
const {useContext, createContext, useState, memo} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
const MemoizedChild = memo(function MemoizedChild() {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
});
|
||||
|
||||
let setValue;
|
||||
function App() {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushAndYield(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushAndYield(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (manual memo)', () => {
|
||||
const {useContext, createContext, useState, memo} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
const MemoizedChild = memo(
|
||||
function MemoizedChild() {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
},
|
||||
function areEqual(prevProps, nextProps) {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
let setValue;
|
||||
function App() {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />, {
|
||||
unstable_isConcurrent: true,
|
||||
});
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushAndYield(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushAndYield(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (function)', () => {
|
||||
const {useContext, createContext, useState} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
function MemoizedChild() {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
let setValue;
|
||||
function App({children}) {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>{children}</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<App>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</App>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushAndYield(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushAndYield(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (forwardRef)', () => {
|
||||
const {forwardRef, useContext, createContext, useState} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
const MemoizedChild = forwardRef(() => {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
});
|
||||
|
||||
let setValue;
|
||||
function App({children}) {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>{children}</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<App>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</App>,
|
||||
{
|
||||
unstable_isConcurrent: true,
|
||||
},
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushAndYield(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushAndYield(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
|
||||
describe('outside concurrent mode', () => {
|
||||
it('a mounted class component can suspend without losing state', () => {
|
||||
class TextWithLifecycle extends React.Component {
|
||||
|
@ -1336,5 +1563,222 @@ describe('ReactSuspense', () => {
|
|||
root.update(<App name="world" />);
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (simple memo)', () => {
|
||||
const {useContext, createContext, useState, memo} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
const MemoizedChild = memo(function MemoizedChild() {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
});
|
||||
|
||||
let setValue;
|
||||
function App() {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />);
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushExpired(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushExpired(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (manual memo)', () => {
|
||||
const {useContext, createContext, useState, memo} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
const MemoizedChild = memo(
|
||||
function MemoizedChild() {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
},
|
||||
function areEqual(prevProps, nextProps) {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
let setValue;
|
||||
function App() {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />);
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushExpired(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushExpired(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (function)', () => {
|
||||
const {useContext, createContext, useState} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
function MemoizedChild() {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
}
|
||||
|
||||
let setValue;
|
||||
function App({children}) {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>
|
||||
{children}
|
||||
</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(
|
||||
<App>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</App>,
|
||||
);
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushExpired(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushExpired(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
|
||||
it('updates memoized child of suspense component when context updates (forwardRef)', () => {
|
||||
const {forwardRef, useContext, createContext, useState} = React;
|
||||
|
||||
const ValueContext = createContext(null);
|
||||
|
||||
const MemoizedChild = forwardRef(function MemoizedChild() {
|
||||
const text = useContext(ValueContext);
|
||||
try {
|
||||
TextResource.read([text, 1000]);
|
||||
Scheduler.unstable_yieldValue(text);
|
||||
return text;
|
||||
} catch (promise) {
|
||||
if (typeof promise.then === 'function') {
|
||||
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
|
||||
} else {
|
||||
Scheduler.unstable_yieldValue(`Error! [${text}]`);
|
||||
}
|
||||
throw promise;
|
||||
}
|
||||
});
|
||||
|
||||
let setValue;
|
||||
function App() {
|
||||
const [value, _setValue] = useState('default');
|
||||
setValue = _setValue;
|
||||
|
||||
return (
|
||||
<ValueContext.Provider value={value}>
|
||||
<Suspense fallback={<Text text="Loading..." />}>
|
||||
<MemoizedChild />
|
||||
</Suspense>
|
||||
</ValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const root = ReactTestRenderer.create(<App />);
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [default]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [default]']);
|
||||
expect(Scheduler).toFlushExpired(['default']);
|
||||
expect(root).toMatchRenderedOutput('default');
|
||||
|
||||
act(() => setValue('new value'));
|
||||
expect(Scheduler).toHaveYielded(['Suspend! [new value]', 'Loading...']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
expect(Scheduler).toHaveYielded(['Promise resolved [new value]']);
|
||||
expect(Scheduler).toFlushExpired(['new value']);
|
||||
expect(root).toMatchRenderedOutput('new value');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue