Skip to content

Commit

Permalink
Flag for client render fallback behavior on hydration mismatch (#22787)
Browse files Browse the repository at this point in the history
* Add flag for new client-render fallback behavior on hydration mismatch

* gate test

* gate tests too

* fix test gating
  • Loading branch information
salazarm committed Nov 18, 2021
1 parent f320ef8 commit fdc1d61
Show file tree
Hide file tree
Showing 14 changed files with 94 additions and 23 deletions.
70 changes: 54 additions & 16 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1680,31 +1680,38 @@ describe('ReactDOMFizzServer', () => {

// @gate experimental
it('calls getServerSnapshot instead of getSnapshot', async () => {
const ref = React.createRef();

function getServerSnapshot() {
return 'server';
}

function getClientSnapshot() {
return 'client';
}

function subscribe() {
return () => {};
}

function Child({text}) {
Scheduler.unstable_yieldValue(text);
return text;
}

function App() {
const value = useSyncExternalStore(
subscribe,
getClientSnapshot,
getServerSnapshot,
);
return (
<div>
<div ref={ref}>
<Child text={value} />
</div>
);
}

const loggedErrors = [];
await act(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(
Expand All @@ -1723,14 +1730,29 @@ describe('ReactDOMFizzServer', () => {

ReactDOM.hydrateRoot(container, <App />);

expect(() => {
// The first paint switches to client rendering due to mismatch
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
expect(() => {
// The first paint switches to client rendering due to mismatch
expect(Scheduler).toFlushUntilNextPaint(['client']);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
} else {
const serverRenderedDiv = container.getElementsByTagName('div')[0];
// The first paint uses the server snapshot
expect(Scheduler).toFlushUntilNextPaint(['server']);
expect(getVisibleChildren(container)).toEqual(<div>server</div>);
// Hydration succeeded
expect(ref.current).toEqual(serverRenderedDiv);

// Asynchronously we detect that the store has changed on the client,
// and patch up the inconsistency
expect(Scheduler).toFlushUntilNextPaint(['client']);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
expect(ref.current).toEqual(serverRenderedDiv);
}
});

// The selector implementation uses the lazy ref initialization pattern
Expand Down Expand Up @@ -1790,15 +1812,31 @@ describe('ReactDOMFizzServer', () => {

ReactDOM.hydrateRoot(container, <App />);

// The first paint uses the client due to mismatch forcing client render
expect(() => {
// The first paint switches to client rendering due to mismatch
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
// The first paint uses the client due to mismatch forcing client render
expect(() => {
// The first paint switches to client rendering due to mismatch
expect(Scheduler).toFlushUntilNextPaint(['client']);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
} else {
const serverRenderedDiv = container.getElementsByTagName('div')[0];

// The first paint uses the server snapshot
expect(Scheduler).toFlushUntilNextPaint(['server']);
expect(getVisibleChildren(container)).toEqual(<div>server</div>);
// Hydration succeeded
expect(ref.current).toEqual(serverRenderedDiv);

// Asynchronously we detect that the store has changed on the client,
// and patch up the inconsistency
expect(Scheduler).toFlushUntilNextPaint(['client']);
}).toErrorDev(
'Warning: An error occurred during hydration. The server HTML was replaced with client content',
{withoutStack: true},
);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
expect(getVisibleChildren(container)).toEqual(<div>client</div>);
expect(ref.current).toEqual(serverRenderedDiv);
}
});

// @gate experimental
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,16 @@ describe('ReactDOMServerPartialHydration', () => {
// hydrating anyway.
suspend = true;
ReactDOM.hydrateRoot(container, <App />);
Scheduler.unstable_flushAll();
if (gate(flags => flags.enableClientRenderFallbackOnHydrationMismatch)) {
Scheduler.unstable_flushAll();
} else {
expect(() => {
Scheduler.unstable_flushAll();
}).toErrorDev(
// TODO: This error should not be logged in this case. It's a false positive.
'Did not expect server HTML to contain the text node "Hello" in <div>.',
);
}
jest.runAllTimers();

// Expect the server-generated HTML to stay intact.
Expand All @@ -215,6 +224,7 @@ describe('ReactDOMServerPartialHydration', () => {
expect(container.textContent).toBe('HelloHello');
});

// @gate enableClientRenderFallbackOnHydrationMismatch
it('falls back to client rendering boundary on mismatch', async () => {
let client = false;
let suspend = false;
Expand Down
12 changes: 9 additions & 3 deletions packages/react-reconciler/src/ReactFiberHydrationContext.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ import {
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {
enableClientRenderFallbackOnHydrationMismatch,
enableSuspenseServerRenderer,
} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.new';
import {
getSuspendedTreeContext,
Expand Down Expand Up @@ -324,8 +327,11 @@ function tryHydrate(fiber, nextInstance) {
}
}

function throwOnHydrationMismatchIfConcurrentMode(fiber) {
if ((fiber.mode & ConcurrentMode) !== NoMode) {
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if (
enableClientRenderFallbackOnHydrationMismatch &&
(fiber.mode & ConcurrentMode) !== NoMode
) {
throw new Error(
'An error occurred during hydration. The server HTML was replaced with client content',
);
Expand Down
12 changes: 9 additions & 3 deletions packages/react-reconciler/src/ReactFiberHydrationContext.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ import {
didNotFindHydratableTextInstance,
didNotFindHydratableSuspenseInstance,
} from './ReactFiberHostConfig';
import {enableSuspenseServerRenderer} from 'shared/ReactFeatureFlags';
import {
enableClientRenderFallbackOnHydrationMismatch,
enableSuspenseServerRenderer,
} from 'shared/ReactFeatureFlags';
import {OffscreenLane} from './ReactFiberLane.old';
import {
getSuspendedTreeContext,
Expand Down Expand Up @@ -324,8 +327,11 @@ function tryHydrate(fiber, nextInstance) {
}
}

function throwOnHydrationMismatchIfConcurrentMode(fiber) {
if ((fiber.mode & ConcurrentMode) !== NoMode) {
function throwOnHydrationMismatchIfConcurrentMode(fiber: Fiber) {
if (
enableClientRenderFallbackOnHydrationMismatch &&
(fiber.mode & ConcurrentMode) !== NoMode
) {
throw new Error(
'An error occurred during hydration. The server HTML was replaced with client content',
);
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/ReactFeatureFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export const enableSuspenseAvoidThisFallback = false;

export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;

export const enableClientRenderFallbackOnHydrationMismatch = true;

export const enableComponentStackLocations = true;

export const enableNewReconciler = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-fb.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
export const enableClientRenderFallbackOnHydrationMismatch = true;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.native-oss.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
export const enableClientRenderFallbackOnHydrationMismatch = true;
export const enableComponentStackLocations = false;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.test-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
export const enableClientRenderFallbackOnHydrationMismatch = true;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const deferRenderPhaseUpdateToNextBatch = false;
export const warnOnSubscriptionInsideStartTransition = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
export const enableClientRenderFallbackOnHydrationMismatch = true;
export const enableStrictEffects = false;
export const createRootStrictEffectsByDefault = false;
export const enableUseRefAccessWarning = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
export const enableClientRenderFallbackOnHydrationMismatch = true;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.testing.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
export const enableClientRenderFallbackOnHydrationMismatch = true;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = false;
export const enableFilterEmptyStringAttributesDOM = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.testing.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const warnAboutSpreadingKeyToJSX = false;
export const warnOnSubscriptionInsideStartTransition = false;
export const enableSuspenseAvoidThisFallback = false;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = false;
export const enableClientRenderFallbackOnHydrationMismatch = true;
export const enableComponentStackLocations = true;
export const enableLegacyFBSupport = !__EXPERIMENTAL__;
export const enableFilterEmptyStringAttributesDOM = false;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.www-dynamic.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const enableSyncDefaultUpdates = __VARIANT__;
export const consoleManagedByDevToolsDuringStrictMode = __VARIANT__;
export const warnOnSubscriptionInsideStartTransition = __VARIANT__;
export const enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay = __VARIANT__;
export const enableClientRenderFallbackOnHydrationMismatch = __VARIANT__;

// Enable this flag to help with concurrent mode debugging.
// It logs information to the console about React scheduling, rendering, and commit phases.
Expand Down
1 change: 1 addition & 0 deletions packages/shared/forks/ReactFeatureFlags.www.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const {
enableSyncDefaultUpdates,
warnOnSubscriptionInsideStartTransition,
enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay,
enableClientRenderFallbackOnHydrationMismatch,
} = dynamicFeatureFlags;

// On WWW, __EXPERIMENTAL__ is used for a new modern build.
Expand Down

0 comments on commit fdc1d61

Please sign in to comment.