Fix: Multiple hydration errors in same render (#23273)
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.
Andrew Clark committed
Feb 10, 2022 at 08:40 UTC
64223fed82414ee41839e95ebc97df330b2b71ca
3 files changed
+70
-6
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+64
@@ -2191,4 +2191,68 @@ describe('ReactDOMFizzServer', () => {
2191
// UI looks normal
2192
expect(container.textContent).toEqual('AB');
2193
});
2194
+
2195
+ // @gate experimental
2196
+ it('logs multiple hydration errors in the same render', async () => {
2197
+ let isClient = false;
2198
+
2199
+ function subscribe() {
2200
+ return () => {};
2201
+ }
2202
+ function getClientSnapshot() {
2203
+ return 'Yay!';
2204
+ }
2205
+ function getServerSnapshot() {
2206
+ if (isClient) {
2207
+ throw new Error('Hydration error');
2208
+ }
2209
+ return 'Yay!';
2210
+ }
2211
+
2212
+ function Child({label}) {
2213
+ // This will throw during client hydration. Only reason to use
2214
+ // useSyncExternalStore in this test is because getServerSnapshot has the
2215
+ // ability to observe whether we're hydrating.
2216
+ useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot);
2217
+ Scheduler.unstable_yieldValue(label);
2218
+ return label;
2219
+ }
2220
+
2221
+ function App() {
2222
+ return (
2223
+ <>
2224
+ <Suspense fallback="Loading...">
2225
+ <Child label="A" />
2226
+ </Suspense>
2227
+ <Suspense fallback="Loading...">
2228
+ <Child label="B" />
2229
+ </Suspense>
2230
+ </>
2231
+ );
2232
+ }
2233
+
2234
+ await act(async () => {
2235
+ const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<App />);
2236
+ pipe(writable);
2237
+ });
2238
+ expect(Scheduler).toHaveYielded(['A', 'B']);
2239
+
2240
+ // Hydrate the tree. Child will throw during hydration, but not when it
2241
+ // falls back to client rendering.
2242
+ isClient = true;
2243
+ ReactDOM.hydrateRoot(container, <App />, {
2244
+ onRecoverableError(error) {
2245
+ Scheduler.unstable_yieldValue(
2246
+ 'Logged recoverable error: ' + error.message,
2247
+ );
2248
+ },
2249
+ });
2250
+
2251
+ expect(Scheduler).toFlushAndYield([
2252
+ 'A',
2253
+ 'B',
2254
+ 'Logged recoverable error: Hydration error',
2255
+ 'Logged recoverable error: Hydration error',
2256
+ ]);
2257
+ });
2258
});
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+3
-3
@@ -920,11 +920,11 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
920
}
921
922
export function queueRecoverableErrors(errors: Array<mixed>) {
923
- if (workInProgressRootConcurrentErrors === null) {
923
+ if (workInProgressRootRecoverableErrors === null) {
924
workInProgressRootRecoverableErrors = errors;
925
} else {
926
- workInProgressRootConcurrentErrors = workInProgressRootConcurrentErrors.push.apply(
927
- workInProgressRootConcurrentErrors,
926
+ workInProgressRootRecoverableErrors.push.apply(
927
+ workInProgressRootRecoverableErrors,
928
errors,
929
);
930
}
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+3
-3
@@ -920,11 +920,11 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
920
}
921
922
export function queueRecoverableErrors(errors: Array<mixed>) {
923
- if (workInProgressRootConcurrentErrors === null) {
923
+ if (workInProgressRootRecoverableErrors === null) {
924
workInProgressRootRecoverableErrors = errors;
925
} else {
926
- workInProgressRootConcurrentErrors = workInProgressRootConcurrentErrors.push.apply(
927
- workInProgressRootConcurrentErrors,
926
+ workInProgressRootRecoverableErrors.push.apply(
927
+ workInProgressRootRecoverableErrors,
928
errors,
929
);
930
}