Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -5040,6 +5040,13 @@ function pingSuspendedRoot(
// the special internal exception that we use to interrupt the stack for
// selective hydration. That was temporarily reverted but we once we add
// it back we can use it here.
//
// In the meantime, record the pinged lanes so markRootSuspended won't
// mark them as suspended, allowing a retry.
workInProgressRootPingedLanes = mergeLanes(
workInProgressRootPingedLanes,
pingedLanes,
);
}
} else {
// Even though we can't restart right now, we might get an
Expand Down
52 changes: 52 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1097,4 +1097,56 @@ describe('ReactDeferredValue', () => {
expect(root).toMatchRenderedOutput(<div>B</div>);
},
);

// Regression test for https://github.com/facebook/react/issues/35821
it('deferred value catches up when a suspension is resolved during the same render', async () => {
let setValue;
function App() {
const [value, _setValue] = useState('initial');
setValue = _setValue;
const deferred = useDeferredValue(value);
return (
<Suspense fallback={<Text text="Loading..." />}>
<AsyncText text={'A:' + deferred} />
<Sibling text={deferred} />
</Suspense>
);
}

function Sibling({text}) {
if (text !== 'initial') {
// Resolve A during this render, simulating data arriving while
// a render is already in progress.
resolveText('A:' + text);
}
readText('B:' + text);
Scheduler.log('B: ' + text);
return text;
}

const root = ReactNoop.createRoot();

resolveText('A:initial');
resolveText('B:initial');
await act(() => root.render(<App />));
assertLog(['A:initial', 'B: initial']);

// Pre-resolve B so the sibling won't suspend on retry.
resolveText('B:updated');

await act(() => setValue('updated'));
assertLog([
// Sync render defers the value.
'A:initial',
'B: initial',
// Deferred render: A suspends, then Sibling resolves A mid-render.
'Suspend! [A:updated]',
'B: updated',
'Loading...',
// React retries and the deferred value catches up.
'A:updated',
'B: updated',
]);
expect(root).toMatchRenderedOutput('A:updatedupdated');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4054,11 +4054,20 @@ describe('ReactSuspenseWithNoopRenderer', () => {
// microtask). But this test shows an example where that's not the case.
//
// The fix was to check if we're in the render phase before calling
// `prepareFreshStack`.
// `prepareFreshStack`. The synchronous ping is instead recorded so the
// lane can be retried.
await act(() => {
startTransition(() => root.render(<App showMore={true} />));
});
assertLog(['Suspend! [A]', 'Loading A...', 'Loading B...']);
assertLog([
'Suspend! [A]',
'Loading A...',
'Loading B...',
// The synchronous ping was recorded, so B retries and renders.
'Suspend! [A]',
'Loading A...',
'B',
]);
expect(root).toMatchRenderedOutput(<div />);
},
);
Expand Down
Loading