Skip to content

Commit 3594b2b

Browse files
committed
third test
1 parent 505c15c commit 3594b2b

File tree

5 files changed

+287
-56
lines changed

5 files changed

+287
-56
lines changed

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

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

476596
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)