Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix: Fix crash when suspending in shell during useSES re-render #27199

Merged
merged 2 commits into from
Aug 8, 2023
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
100 changes: 39 additions & 61 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -892,58 +892,39 @@ export function performConcurrentWorkOnRoot(
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (exitStatus !== RootInProgress) {
if (exitStatus === RootErrored) {
// If something threw an error, try rendering one more time. We'll
// render synchronously to block concurrent data mutations, and we'll
// includes all pending updates are included. If it still fails after
// the second attempt, we'll give up and commit the resulting tree.
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
}
}
if (exitStatus === RootFatalErrored) {
const fatalError = workInProgressRootFatalError;
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes);
ensureRootIsScheduled(root);
throw fatalError;
}

if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(root, lanes);
} else {
// The render completed.

// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
const renderWasConcurrent = !includesBlockingLane(root, lanes);
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes);
if (exitStatus !== RootInProgress) {
let renderWasConcurrent = shouldTimeSlice;
do {
if (exitStatus === RootDidNotComplete) {
// The render unwound without completing the tree. This happens in special
// cases where need to exit the current render without producing a
// consistent tree or committing.
markRootSuspended(root, lanes);
} else {
// The render completed.

// Check if this render may have yielded to a concurrent event, and if so,
// confirm that any newly rendered stores are consistent.
// TODO: It's possible that even a concurrent render may never have yielded
// to the main thread, if it was fast enough, or if it expired. We could
// skip the consistency check in that case, too.
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
// A store was mutated in an interleaved event. Render again,
// synchronously, to block further mutations.
exitStatus = renderRootSync(root, lanes);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
renderWasConcurrent = false;
// Need to check the exit status again.
continue;
}

// We need to check again if something threw
// Check if something threw
if (exitStatus === RootErrored) {
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
Expand All @@ -957,8 +938,7 @@ export function performConcurrentWorkOnRoot(
originallyAttemptedLanes,
errorRetryLanes,
);
// We assume the tree is now consistent because we didn't yield to any
// concurrent events.
renderWasConcurrent = false;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for clarity, right? As far as I can tell, it's not read anymore at this point.

Copy link
Collaborator Author

@acdlite acdlite Aug 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah :D in case a continue gets added later in the loop or something

}
}
if (exitStatus === RootFatalErrored) {
Expand All @@ -969,16 +949,14 @@ export function performConcurrentWorkOnRoot(
throw fatalError;
}

// FIXME: Need to check for RootDidNotComplete again. The factoring here
// isn't ideal.
// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}

// We now have a consistent tree. The next step is either to commit it,
// or, if something suspended, wait to commit it after a timeout.
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
}

ensureRootIsScheduled(root);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ let forwardRef;
let useImperativeHandle;
let useRef;
let useState;
let use;
let startTransition;
let waitFor;
let waitForAll;
Expand All @@ -41,6 +42,7 @@ describe('useSyncExternalStore', () => {
forwardRef = React.forwardRef;
useRef = React.useRef;
useState = React.useState;
use = React.use;
useSyncExternalStore = React.useSyncExternalStore;
startTransition = React.startTransition;

Expand Down Expand Up @@ -212,4 +214,84 @@ describe('useSyncExternalStore', () => {
});
assertLog(['value:initial']);
});

test(
'regression: suspending in shell after synchronously patching ' +
'up store mutation',
async () => {
// Tests a case where a store is mutated during a concurrent event, then
// during the sync re-render, a synchronous render is triggered.

const store = createExternalStore('Initial');

let resolve;
const promise = new Promise(r => {
resolve = r;
});

function A() {
const value = useSyncExternalStore(store.subscribe, store.getState);

if (value === 'Updated') {
try {
use(promise);
} catch (x) {
Scheduler.log('Suspend A');
throw x;
}
}

return <Text text={'A: ' + value} />;
}

function B() {
const value = useSyncExternalStore(store.subscribe, store.getState);
return <Text text={'B: ' + value} />;
}

function App() {
return (
<>
<span>
<A />
</span>
<span>
<B />
</span>
</>
);
}

const root = ReactNoop.createRoot();
await act(async () => {
// A and B both read from the same store. Partially render A.
startTransition(() => root.render(<App />));
// A reads the initial value of the store.
await waitFor(['A: Initial']);

// Before B renders, mutate the store.
store.set('Updated');
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before the fix, the test would crash here with an internal error ("Unknown root status")

assertLog([
// B reads the updated value of the store.
'B: Updated',
// This should a synchronous re-render of A using the updated value. In
// this test, this causes A to suspend.
'Suspend A',
]);
// Nothing has committed, because A suspended and no fallback
// was provided.
expect(root).toMatchRenderedOutput(null);

// Resolve the data and finish rendering.
await act(() => resolve());
assertLog(['A: Updated', 'B: Updated']);
expect(root).toMatchRenderedOutput(
<>
<span>A: Updated</span>
<span>B: Updated</span>
</>,
);
},
);
});