Skip to content

Commit 5c43c6f

Browse files
authored
Unwind the current workInProgress if it's suspended (#25247)
Usually we complete workInProgress before yielding but if that's the currently suspended one, we don't yet complete it in case we can immediately unblock it. If we get interrupted, however, we must unwind it. Where as we usually assume that we've already completed it. This shows up when the current work in progress was a Context that pushed and then it suspends in its immediate children. If we don't unwind, it won't pop and so we get an imbalance.
1 parent e52fa4c commit 5c43c6f

File tree

3 files changed

+53
-2
lines changed

3 files changed

+53
-2
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.new.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,9 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
16511651
}
16521652

16531653
if (workInProgress !== null) {
1654-
let interruptedWork = workInProgress.return;
1654+
let interruptedWork = workInProgressIsSuspended
1655+
? workInProgress
1656+
: workInProgress.return;
16551657
while (interruptedWork !== null) {
16561658
const current = interruptedWork.alternate;
16571659
unwindInterruptedWork(

packages/react-reconciler/src/ReactFiberWorkLoop.old.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,9 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
16511651
}
16521652

16531653
if (workInProgress !== null) {
1654-
let interruptedWork = workInProgress.return;
1654+
let interruptedWork = workInProgressIsSuspended
1655+
? workInProgress
1656+
: workInProgress.return;
16551657
while (interruptedWork !== null) {
16561658
const current = interruptedWork.alternate;
16571659
unwindInterruptedWork(

packages/react-reconciler/src/__tests__/ReactWakeable-test.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,51 @@ describe('ReactWakeable', () => {
339339
expect(Scheduler).toFlushWithoutYielding();
340340
expect(root).toMatchRenderedOutput('AB');
341341
});
342+
343+
// @gate enableUseHook
344+
test('interrupting while yielded should reset contexts', async () => {
345+
let resolve;
346+
const promise = new Promise(r => {
347+
resolve = r;
348+
});
349+
350+
const Context = React.createContext();
351+
352+
const lazy = React.lazy(() => {
353+
return promise;
354+
});
355+
356+
function ContextText() {
357+
return <Text text={use(Context)} />;
358+
}
359+
360+
function App({text}) {
361+
return (
362+
<div>
363+
<Context.Provider value={text}>
364+
{lazy}
365+
<ContextText />
366+
</Context.Provider>
367+
</div>
368+
);
369+
}
370+
371+
const root = ReactNoop.createRoot();
372+
startTransition(() => {
373+
root.render(<App text="world" />);
374+
});
375+
expect(Scheduler).toFlushUntilNextPaint([]);
376+
expect(root).toMatchRenderedOutput(null);
377+
378+
await resolve({default: <Text key="hi" text="Hello " />});
379+
380+
// Higher priority update that interrupts the first render
381+
ReactNoop.flushSync(() => {
382+
root.render(<App text="world!" />);
383+
});
384+
385+
expect(Scheduler).toHaveYielded(['Hello ', 'world!']);
386+
387+
expect(root).toMatchRenderedOutput(<div>Hello world!</div>);
388+
});
342389
});

0 commit comments

Comments
 (0)