Skip to content

Commit 272559b

Browse files
committed
Fix: Multiple hydration errors in same render
I made a minor mistake in the original onRecoverableError PR that only surfaces if there are hydration errors in two different Suspense boundaries in the same render. This fixes it and adds a unit test.
1 parent efd8f64 commit 272559b

3 files changed

Lines changed: 69 additions & 6 deletions

File tree

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,4 +2191,67 @@ describe('ReactDOMFizzServer', () => {
21912191
// UI looks normal
21922192
expect(container.textContent).toEqual('AB');
21932193
});
2194+
2195+
it('logs multiple hydration errors in the same render', async () => {
2196+
let isClient = false;
2197+
2198+
function subscribe() {
2199+
return () => {};
2200+
}
2201+
function getClientSnapshot() {
2202+
return 'Yay!';
2203+
}
2204+
function getServerSnapshot() {
2205+
if (isClient) {
2206+
throw new Error('Hydration error');
2207+
}
2208+
return 'Yay!';
2209+
}
2210+
2211+
function Child({label}) {
2212+
// This will throw during client hydration. Only reason to use
2213+
// useSyncExternalStore in this test is because getServerSnapshot has the
2214+
// ability to observe whether we're hydrating.
2215+
useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot);
2216+
Scheduler.unstable_yieldValue(label);
2217+
return label;
2218+
}
2219+
2220+
function App() {
2221+
return (
2222+
<>
2223+
<Suspense fallback="Loading...">
2224+
<Child label="A" />
2225+
</Suspense>
2226+
<Suspense fallback="Loading...">
2227+
<Child label="B" />
2228+
</Suspense>
2229+
</>
2230+
);
2231+
}
2232+
2233+
await act(async () => {
2234+
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
2235+
pipe(writable);
2236+
});
2237+
expect(Scheduler).toHaveYielded(['A', 'B']);
2238+
2239+
// Hydrate the tree. Child will throw during hydration, but not when it
2240+
// falls back to client rendering.
2241+
isClient = true;
2242+
ReactDOM.hydrateRoot(container, <App />, {
2243+
onRecoverableError(error) {
2244+
Scheduler.unstable_yieldValue(
2245+
'Logged recoverable error: ' + error.message,
2246+
);
2247+
},
2248+
});
2249+
2250+
expect(Scheduler).toFlushAndYield([
2251+
'A',
2252+
'B',
2253+
'Logged recoverable error: Hydration error',
2254+
'Logged recoverable error: Hydration error',
2255+
]);
2256+
});
21942257
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -920,11 +920,11 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
920920
}
921921

922922
export function queueRecoverableErrors(errors: Array<mixed>) {
923-
if (workInProgressRootConcurrentErrors === null) {
923+
if (workInProgressRootRecoverableErrors === null) {
924924
workInProgressRootRecoverableErrors = errors;
925925
} else {
926-
workInProgressRootConcurrentErrors = workInProgressRootConcurrentErrors.push.apply(
927-
workInProgressRootConcurrentErrors,
926+
workInProgressRootRecoverableErrors.push.apply(
927+
workInProgressRootRecoverableErrors,
928928
errors,
929929
);
930930
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -920,11 +920,11 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
920920
}
921921

922922
export function queueRecoverableErrors(errors: Array<mixed>) {
923-
if (workInProgressRootConcurrentErrors === null) {
923+
if (workInProgressRootRecoverableErrors === null) {
924924
workInProgressRootRecoverableErrors = errors;
925925
} else {
926-
workInProgressRootConcurrentErrors = workInProgressRootConcurrentErrors.push.apply(
927-
workInProgressRootConcurrentErrors,
926+
workInProgressRootRecoverableErrors.push.apply(
927+
workInProgressRootRecoverableErrors,
928928
errors,
929929
);
930930
}

0 commit comments

Comments
 (0)