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
68 changes: 68 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,74 @@ describe('ReactDOMFiberAsync', () => {
});
});

it('regression: useDeferredValue in popState leads to infinite deferral loop', async () => {
// At the time this test was written, it simulated a particular crash that
// was happened due to a combination of very subtle implementation details.
// Rather than couple this test to those implementation details, I've chosen
// to keep it as high-level as possible so that it doesn't break if the
// details change. In the future, it might not be trigger the exact set of
// internal circumstances anymore, but it could be useful for catching
// similar bugs because it represents a realistic real world situation —
// namely, switching tabs repeatedly in an app that uses useDeferredValue.
//
// But don't worry too much about why this test is written the way it is.

// Represents the browser's current location
let browserPathname = '/path/a';

let setPathname;
function App({initialPathname}) {
const [pathname, _setPathname] = React.useState('/path/a');
setPathname = _setPathname;

const deferredPathname = React.useDeferredValue(pathname);

// Attach a popstate listener on mount. Normally this would be in the
// in the router implementation.
React.useEffect(() => {
function onPopstate() {
React.startTransition(() => {
setPathname(browserPathname);
});
}
window.addEventListener('popstate', onPopstate);
return () => window.removeEventListener('popstate', onPopstate);
}, []);

return `Current: ${pathname}\nDeferred: ${deferredPathname}`;
}

const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App initialPathname={browserPathname} />);
});

// Simulate a series of popstate events that toggle back and forth between
// two locations. In the original regression case, a certain combination
// of transition lanes would cause React to fall into an infinite deferral
// loop — specifically, when the spawned by the useDeferredValue hook was
// assigned a "higher" bit value than the one assigned to the "popstate".

// For alignment reasons, call this once to advance the internal variable
// that assigns transition lanes. Because this is a no-op update, it will
// bump the counter, but it won't trigger the useDeferredValue hook.
setPathname(browserPathname);

// Trigger enough popstate events that the scenario occurs for every
// possible transition lane.
for (let i = 0; i < 50; i++) {
await act(async () => {
// Simulate a popstate event
browserPathname = browserPathname === '/path/a' ? '/path/b' : '/path/a';
const popStateEvent = new Event('popstate');
window.event = popStateEvent;
window.dispatchEvent(popStateEvent);
await waitForMicrotasks();
window.event = undefined;
});
}
});

it('regression: infinite deferral loop caused by unstable useDeferredValue input', async () => {
function Text({text}) {
Scheduler.log(text);
Expand Down
4 changes: 3 additions & 1 deletion packages/react-reconciler/src/ReactFiberHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3033,7 +3033,9 @@ function updateDeferredValueImpl<T>(
return resultValue;
}

const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
const shouldDeferValue =
!includesOnlyNonUrgentLanes(renderLanes) &&
!includesSomeLane(renderLanes, DeferredLane);
if (shouldDeferValue) {
// This is an urgent update. Since the value has changed, keep using the
// previous value and spawn a deferred render to update it later.
Expand Down
Loading