@samitouri / QOS-React-2 / commits / 5c43c6f026

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.

Sebastian Markbåge committed Sep 13, 2022 at 11:18 UTC 5c43c6f02652b0a3f66678a560e84d3110c573b7
3 files changed +53 -2
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+3 -1
@@ -1651,7 +1651,9 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1651 }
1652
1653 if (workInProgress !== null) {
1654 - let interruptedWork = workInProgress.return;
1654 + let interruptedWork = workInProgressIsSuspended
1655 + ? workInProgress
1656 + : workInProgress.return;
1657 while (interruptedWork !== null) {
1658 const current = interruptedWork.alternate;
1659 unwindInterruptedWork(
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+3 -1
@@ -1651,7 +1651,9 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
1651 }
1652
1653 if (workInProgress !== null) {
1654 - let interruptedWork = workInProgress.return;
1654 + let interruptedWork = workInProgressIsSuspended
1655 + ? workInProgress
1656 + : workInProgress.return;
1657 while (interruptedWork !== null) {
1658 const current = interruptedWork.alternate;
1659 unwindInterruptedWork(
packages/react-reconciler/src/__tests__/ReactWakeable-test.js
+47
@@ -339,4 +339,51 @@ describe('ReactWakeable', () => {
339 expect(Scheduler).toFlushWithoutYielding();
340 expect(root).toMatchRenderedOutput('AB');
341 });
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 + });
389 });