Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flag for client render fallback behavior on hydration mismatch #22787

Merged
merged 4 commits into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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