@samitouri / QOS-React / commits / 64223fed82

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 }