From c0d218f0f3e02ed581f74096e56baa947213135d Mon Sep 17 00:00:00 2001 From: dan Date: Tue, 24 Mar 2026 00:49:05 +0000 Subject: [PATCH] Fix useDeferredValue getting stuck (#36134) Fixes https://github.com/facebook/react/issues/35821 Written/debugged by Claude. ## Test Plan - Verify undoing the source fix fails the newly added test - Verify building a bundle with the fix solves https://github.com/gaearon/react-udv-bug/ repro --- .../src/ReactFiberWorkLoop.js | 7 +++ .../src/__tests__/ReactDeferredValue-test.js | 52 +++++++++++++++++++ .../ReactSuspenseWithNoopRenderer-test.js | 13 ++++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index 15a260bc660c..6eed36e6d878 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -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 diff --git a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js index 3a348307f4cd..2455d344cf5c 100644 --- a/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js +++ b/packages/react-reconciler/src/__tests__/ReactDeferredValue-test.js @@ -1097,4 +1097,56 @@ describe('ReactDeferredValue', () => { expect(root).toMatchRenderedOutput(
B
); }, ); + + // 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 ( + }> + + + + ); + } + + 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()); + 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'); + }); }); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js index a5c4282e9e78..44d784788b21 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js @@ -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()); }); - 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(
); }, );