Skip to content

Commit 2e2efb6

Browse files
committed
gate console errors to __DEV__
1 parent 505c15c commit 2e2efb6

File tree

5 files changed

+316
-60
lines changed

5 files changed

+316
-60
lines changed

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,7 @@ describe('ReactDOMServerPartialHydration', () => {
437437
expect(container.innerHTML).toContain('<div>Sibling</div>');
438438
});
439439

440-
it('recovers when server rendered additional nodes', async () => {
440+
it('recovers with client render when server rendered additional nodes at suspense root', async () => {
441441
const ref = React.createRef();
442442
function App({hasB}) {
443443
return (
@@ -462,15 +462,143 @@ describe('ReactDOMServerPartialHydration', () => {
462462
expect(container.innerHTML).toContain('<span>B</span>');
463463
expect(ref.current).toBe(null);
464464

465-
ReactDOM.hydrateRoot(container, <App hasB={false} />);
466465
expect(() => {
467-
Scheduler.unstable_flushAll();
466+
act(() => {
467+
ReactDOM.hydrateRoot(container, <App hasB={false} />);
468+
});
468469
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
470+
469471
jest.runAllTimers();
470472

471473
expect(container.innerHTML).toContain('<span>A</span>');
472474
expect(container.innerHTML).not.toContain('<span>B</span>');
473-
expect(ref.current).toBe(span);
475+
476+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
477+
expect(ref.current).not.toBe(span);
478+
} else {
479+
expect(ref.current).toBe(span);
480+
}
481+
});
482+
483+
it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
484+
spyOnDev(console, 'error');
485+
const ref = React.createRef();
486+
function App({hasB}) {
487+
return (
488+
<div>
489+
<Suspense fallback="Loading...">
490+
<Suspender />
491+
<span ref={ref}>A</span>
492+
{hasB ? <span>B</span> : null}
493+
</Suspense>
494+
<div>Sibling</div>
495+
</div>
496+
);
497+
}
498+
499+
let shouldSuspend = false;
500+
let resolve;
501+
const promise = new Promise(res => {
502+
resolve = () => {
503+
shouldSuspend = false;
504+
res();
505+
};
506+
});
507+
function Suspender() {
508+
if (shouldSuspend) {
509+
throw promise;
510+
}
511+
return <div />;
512+
}
513+
514+
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
515+
516+
const container = document.createElement('div');
517+
container.innerHTML = finalHTML;
518+
519+
const span = container.getElementsByTagName('span')[0];
520+
521+
expect(container.innerHTML).toContain('<span>A</span>');
522+
expect(container.innerHTML).toContain('<span>B</span>');
523+
expect(ref.current).toBe(null);
524+
525+
shouldSuspend = true;
526+
act(() => {
527+
ReactDOM.hydrateRoot(container, <App hasB={false} />);
528+
});
529+
530+
resolve();
531+
await promise;
532+
Scheduler.unstable_flushAll();
533+
await null;
534+
jest.runAllTimers();
535+
536+
expect(container.innerHTML).toContain('<span>A</span>');
537+
expect(container.innerHTML).not.toContain('<span>B</span>');
538+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
539+
expect(ref.current).not.toBe(span);
540+
} else {
541+
expect(ref.current).toBe(span);
542+
}
543+
544+
if (__DEV__) {
545+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
546+
expect(console.error.calls.count()).toBe(2);
547+
const errorArgs = console.error.calls.all().shift().args;
548+
expect(errorArgs[0]).toContain(
549+
'An error occurred during hydration. The server HTML was replaced with client content',
550+
);
551+
} else {
552+
expect(console.error.calls.count()).toBe(1);
553+
}
554+
555+
const errorArgs = console.error.calls.all().pop().args;
556+
expect(errorArgs[0]).toBe(
557+
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
558+
);
559+
}
560+
});
561+
562+
it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {
563+
const ref = React.createRef();
564+
function App({hasB}) {
565+
return (
566+
<div>
567+
<Suspense fallback="Loading...">
568+
<div>
569+
<span ref={ref}>A</span>
570+
{hasB ? <span>B</span> : null}
571+
</div>
572+
</Suspense>
573+
<div>Sibling</div>
574+
</div>
575+
);
576+
}
577+
578+
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
579+
580+
const container = document.createElement('div');
581+
container.innerHTML = finalHTML;
582+
583+
const span = container.getElementsByTagName('span')[0];
584+
585+
expect(container.innerHTML).toContain('<span>A</span>');
586+
expect(container.innerHTML).toContain('<span>B</span>');
587+
expect(ref.current).toBe(null);
588+
589+
expect(() => {
590+
act(() => {
591+
ReactDOM.hydrateRoot(container, <App hasB={false} />);
592+
});
593+
}).toErrorDev('Did not expect server HTML to contain a <span> in <div>');
594+
595+
expect(container.innerHTML).toContain('<span>A</span>');
596+
expect(container.innerHTML).not.toContain('<span>B</span>');
597+
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
598+
expect(ref.current).not.toBe(span);
599+
} else {
600+
expect(ref.current).toBe(span);
601+
}
474602
});
475603

476604
it('calls the onDeleted hydration callback if the parent gets deleted', async () => {

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

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ import type {
2929
import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
3030
import type {OffscreenState} from './ReactFiberOffscreenComponent';
3131
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
32-
import {enableSuspenseAvoidThisFallback} from 'shared/ReactFeatureFlags';
32+
import {
33+
enableClientRenderFallbackOnHydrationMismatch,
34+
enableSuspenseAvoidThisFallback,
35+
} from 'shared/ReactFeatureFlags';
3336

3437
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
3538

@@ -74,6 +77,9 @@ import {
7477
StaticMask,
7578
MutationMask,
7679
Passive,
80+
Incomplete,
81+
ShouldCapture,
82+
ForceClientRender,
7783
} from './ReactFiberFlags';
7884

7985
import {
@@ -120,9 +126,11 @@ import {
120126
prepareToHydrateHostInstance,
121127
prepareToHydrateHostTextInstance,
122128
prepareToHydrateHostSuspenseInstance,
129+
warnDeleteNextHydratableInstance,
123130
popHydrationState,
124131
resetHydrationState,
125132
getIsHydrating,
133+
hasMore,
126134
} from './ReactFiberHydrationContext.new';
127135
import {
128136
enableSuspenseCallback,
@@ -828,7 +836,6 @@ function completeWork(
828836
// to the current tree provider fiber is just as fast and less error-prone.
829837
// Ideally we would have a special version of the work loop only
830838
// for hydration.
831-
popTreeContext(workInProgress);
832839
switch (workInProgress.tag) {
833840
case IndeterminateComponent:
834841
case LazyComponent:
@@ -840,9 +847,11 @@ function completeWork(
840847
case Profiler:
841848
case ContextConsumer:
842849
case MemoComponent:
850+
popTreeContext(workInProgress);
843851
bubbleProperties(workInProgress);
844852
return null;
845853
case ClassComponent: {
854+
popTreeContext(workInProgress);
846855
const Component = workInProgress.type;
847856
if (isLegacyContextProvider(Component)) {
848857
popLegacyContext(workInProgress);
@@ -852,6 +861,23 @@ function completeWork(
852861
}
853862
case HostRoot: {
854863
const fiberRoot = (workInProgress.stateNode: FiberRoot);
864+
if (current === null || current.child === null) {
865+
// If we hydrated, pop so that we can delete any remaining children
866+
// that weren't hydrated.
867+
const wasHydrated = popHydrationState(workInProgress);
868+
if (wasHydrated) {
869+
// If we hydrated, then we'll need to schedule an update for
870+
// the commit side-effects on the root.
871+
markUpdate(workInProgress);
872+
} else if (!fiberRoot.isDehydrated) {
873+
// Schedule an effect to clear this container at the start of the next commit.
874+
// This handles the case of React rendering into a container with previous children.
875+
// It's also safe to do for updates too, because current.child would only be null
876+
// if the previous render was null (so the container would already be empty).
877+
workInProgress.flags |= Snapshot;
878+
}
879+
}
880+
popTreeContext(workInProgress);
855881
if (enableCache) {
856882
popRootCachePool(fiberRoot, renderLanes);
857883

@@ -873,27 +899,13 @@ function completeWork(
873899
fiberRoot.context = fiberRoot.pendingContext;
874900
fiberRoot.pendingContext = null;
875901
}
876-
if (current === null || current.child === null) {
877-
// If we hydrated, pop so that we can delete any remaining children
878-
// that weren't hydrated.
879-
const wasHydrated = popHydrationState(workInProgress);
880-
if (wasHydrated) {
881-
// If we hydrated, then we'll need to schedule an update for
882-
// the commit side-effects on the root.
883-
markUpdate(workInProgress);
884-
} else if (!fiberRoot.isDehydrated) {
885-
// Schedule an effect to clear this container at the start of the next commit.
886-
// This handles the case of React rendering into a container with previous children.
887-
// It's also safe to do for updates too, because current.child would only be null
888-
// if the previous render was null (so the container would already be empty).
889-
workInProgress.flags |= Snapshot;
890-
}
891-
}
892902
updateHostContainer(current, workInProgress);
893903
bubbleProperties(workInProgress);
894904
return null;
895905
}
896906
case HostComponent: {
907+
const wasHydrated = popHydrationState(workInProgress);
908+
popTreeContext(workInProgress);
897909
popHostContext(workInProgress);
898910
const rootContainerInstance = getRootHostContainer();
899911
const type = workInProgress.type;
@@ -928,7 +940,6 @@ function completeWork(
928940
// "stack" as the parent. Then append children as we go in beginWork
929941
// or completeWork depending on whether we want to add them top->down or
930942
// bottom->up. Top->down is faster in IE11.
931-
const wasHydrated = popHydrationState(workInProgress);
932943
if (wasHydrated) {
933944
// TODO: Move this and createInstance step into the beginPhase
934945
// to consolidate.
@@ -981,6 +992,7 @@ function completeWork(
981992
return null;
982993
}
983994
case HostText: {
995+
popTreeContext(workInProgress);
984996
const newText = newProps;
985997
if (current && workInProgress.stateNode != null) {
986998
const oldText = current.memoizedProps;
@@ -1017,14 +1029,27 @@ function completeWork(
10171029
return null;
10181030
}
10191031
case SuspenseComponent: {
1020-
popSuspenseContext(workInProgress);
10211032
const nextState: null | SuspenseState = workInProgress.memoizedState;
1022-
10231033
if (enableSuspenseServerRenderer) {
1034+
if (
1035+
enableClientRenderFallbackOnHydrationMismatch &&
1036+
hasMore() &&
1037+
(workInProgress.flags & DidCapture) === NoFlags
1038+
) {
1039+
warnDeleteNextHydratableInstance(workInProgress);
1040+
resetHydrationState();
1041+
workInProgress.flags |=
1042+
ForceClientRender | Incomplete | ShouldCapture;
1043+
popTreeContext(workInProgress);
1044+
popSuspenseContext(workInProgress);
1045+
return workInProgress;
1046+
}
10241047
if (nextState !== null && nextState.dehydrated !== null) {
10251048
// We might be inside a hydration state the first time we're picking up this
10261049
// Suspense boundary, and also after we've reentered it for further hydration.
10271050
const wasHydrated = popHydrationState(workInProgress);
1051+
popTreeContext(workInProgress);
1052+
popSuspenseContext(workInProgress);
10281053
if (current === null) {
10291054
if (!wasHydrated) {
10301055
throw new Error(
@@ -1091,6 +1116,8 @@ function completeWork(
10911116
) {
10921117
transferActualDuration(workInProgress);
10931118
}
1119+
popTreeContext(workInProgress);
1120+
popSuspenseContext(workInProgress);
10941121
// Don't bubble properties in this case.
10951122
return workInProgress;
10961123
}
@@ -1103,6 +1130,8 @@ function completeWork(
11031130
const prevState: null | SuspenseState = current.memoizedState;
11041131
prevDidTimeout = prevState !== null;
11051132
}
1133+
popTreeContext(workInProgress);
1134+
popSuspenseContext(workInProgress);
11061135

11071136
if (enableCache && nextDidTimeout) {
11081137
const offscreenFiber: Fiber = (workInProgress.child: any);
@@ -1207,6 +1236,7 @@ function completeWork(
12071236
return null;
12081237
}
12091238
case HostPortal:
1239+
popTreeContext(workInProgress);
12101240
popHostContainer(workInProgress);
12111241
updateHostContainer(current, workInProgress);
12121242
if (current === null) {
@@ -1215,12 +1245,14 @@ function completeWork(
12151245
bubbleProperties(workInProgress);
12161246
return null;
12171247
case ContextProvider:
1248+
popTreeContext(workInProgress);
12181249
// Pop provider fiber
12191250
const context: ReactContext<any> = workInProgress.type._context;
12201251
popProvider(context, workInProgress);
12211252
bubbleProperties(workInProgress);
12221253
return null;
12231254
case IncompleteClassComponent: {
1255+
popTreeContext(workInProgress);
12241256
// Same as class component case. I put it down here so that the tags are
12251257
// sequential to ensure this switch is compiled to a jump table.
12261258
const Component = workInProgress.type;
@@ -1231,6 +1263,7 @@ function completeWork(
12311263
return null;
12321264
}
12331265
case SuspenseListComponent: {
1266+
popTreeContext(workInProgress);
12341267
popSuspenseContext(workInProgress);
12351268

12361269
const renderState: null | SuspenseListRenderState =
@@ -1440,6 +1473,7 @@ function completeWork(
14401473
return null;
14411474
}
14421475
case ScopeComponent: {
1476+
popTreeContext(workInProgress);
14431477
if (enableScopeAPI) {
14441478
if (current === null) {
14451479
const scopeInstance: ReactScopeInstance = createScopeInstance();
@@ -1464,6 +1498,7 @@ function completeWork(
14641498
}
14651499
case OffscreenComponent:
14661500
case LegacyHiddenComponent: {
1501+
popTreeContext(workInProgress);
14671502
popRenderLanes(workInProgress);
14681503
const nextState: OffscreenState | null = workInProgress.memoizedState;
14691504
const nextIsHidden = nextState !== null;
@@ -1532,6 +1567,7 @@ function completeWork(
15321567
return null;
15331568
}
15341569
case CacheComponent: {
1570+
popTreeContext(workInProgress);
15351571
if (enableCache) {
15361572
let previousCache: Cache | null = null;
15371573
if (workInProgress.alternate !== null) {

0 commit comments

Comments
 (0)