.',
@@ -1820,7 +1786,7 @@ describe('ReactDOMServerHooks', () => {
);
});
- it('useOpaqueIdentifier throws if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
+ it('useOpaqueIdentifier warns if you try to use the result as a string in a child component wrapped in a Suspense', async () => {
function Child({appId}) {
return
;
}
@@ -1842,16 +1808,14 @@ describe('ReactDOMServerHooks', () => {
,
);
- if (
- gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
- ) {
+ if (gate(flags => !flags.new)) {
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
} else {
- // In the old reconciler, the error isn't surfaced to the user. That
- // part isn't important, as long as It warns.
+ // This error isn't surfaced to the user; only the warning is.
+ // The error is just the mechanism that restarts the render.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
@@ -1864,7 +1828,7 @@ describe('ReactDOMServerHooks', () => {
}
});
- it('useOpaqueIdentifier throws if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
+ it('useOpaqueIdentifier warns if you try to add the result as a number in a child component wrapped in a Suspense', async () => {
function Child({appId}) {
return
;
}
@@ -1888,16 +1852,14 @@ describe('ReactDOMServerHooks', () => {
,
);
- if (
- gate(flags => flags.new && flags.deferRenderPhaseUpdateToNextBatch)
- ) {
+ if (gate(flags => !flags.new)) {
expect(() => Scheduler.unstable_flushAll()).toErrorDev([
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
'Do not read the value directly.',
]);
} else {
- // In the old reconciler, the error isn't surfaced to the user. That
- // part isn't important, as long as It warns.
+ // This error isn't surfaced to the user; only the warning is.
+ // The error is just the mechanism that restarts the render.
expect(() =>
expect(() => Scheduler.unstable_flushAll()).toThrow(
'The object passed back from useOpaqueIdentifier is meant to be passed through to attributes only. ' +
diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js
index 3c29c2e86a6..549f7c3e600 100644
--- a/packages/react-dom/src/client/ReactDOMRoot.js
+++ b/packages/react-dom/src/client/ReactDOMRoot.js
@@ -9,9 +9,8 @@
import type {Container} from './ReactDOMHostConfig';
import type {RootTag} from 'react-reconciler/src/ReactRootTags';
-import type {ReactNodeList} from 'shared/ReactTypes';
+import type {MutableSource, ReactNodeList} from 'shared/ReactTypes';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
-import {findHostInstanceWithNoPortals} from 'react-reconciler/src/ReactFiberReconciler';
export type RootType = {
render(children: ReactNodeList): void,
@@ -25,6 +24,7 @@ export type RootOptions = {
hydrationOptions?: {
onHydrated?: (suspenseNode: Comment) => void,
onDeleted?: (suspenseNode: Comment) => void,
+ mutableSources?: Array
>,
...
},
...
@@ -47,6 +47,8 @@ import {ensureListeningTo} from './ReactDOMComponent';
import {
createContainer,
updateContainer,
+ findHostInstanceWithNoPortals,
+ registerMutableSourceForHydration,
} from 'react-reconciler/src/ReactFiberReconciler';
import invariant from 'shared/invariant';
import {
@@ -124,6 +126,11 @@ function createRootImpl(
const hydrate = options != null && options.hydrate === true;
const hydrationCallbacks =
(options != null && options.hydrationOptions) || null;
+ const mutableSources =
+ (options != null &&
+ options.hydrationOptions != null &&
+ options.hydrationOptions.mutableSources) ||
+ null;
const root = createContainer(container, tag, hydrate, hydrationCallbacks);
markContainerAsRoot(root.current, container);
const containerNodeType = container.nodeType;
@@ -143,6 +150,14 @@ function createRootImpl(
) {
ensureListeningTo(container, 'onMouseEnter');
}
+
+ if (mutableSources) {
+ for (let i = 0; i < mutableSources.length; i++) {
+ const mutableSource = mutableSources[i];
+ registerMutableSourceForHydration(root, mutableSource);
+ }
+ }
+
return root;
}
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
index 76838eda06e..38cbec836fe 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js
@@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {Lanes, Lane} from './ReactFiberLane';
+import type {MutableSource} from 'shared/ReactTypes';
import type {
SuspenseState,
SuspenseListRenderState,
@@ -126,6 +127,7 @@ import {
isSuspenseInstancePending,
isSuspenseInstanceFallback,
registerSuspenseInstanceRetry,
+ supportsHydration,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldSuspend} from './ReactFiberReconciler';
@@ -193,8 +195,12 @@ import {
markSkippedUpdateLanes,
getWorkInProgressRoot,
pushRenderLanes,
+ getExecutionContext,
+ RetryAfterError,
+ NoContext,
} from './ReactFiberWorkLoop.new';
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
+import {setWorkInProgressVersion} from './ReactMutableSource.new';
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -1071,6 +1077,20 @@ function updateHostRoot(current, workInProgress, renderLanes) {
// be any children to hydrate which is effectively the same thing as
// not hydrating.
+ if (supportsHydration) {
+ const mutableSourceEagerHydrationData =
+ root.mutableSourceEagerHydrationData;
+ if (mutableSourceEagerHydrationData != null) {
+ for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
+ const mutableSource = ((mutableSourceEagerHydrationData[
+ i
+ ]: any): MutableSource);
+ const version = mutableSourceEagerHydrationData[i + 1];
+ setWorkInProgressVersion(mutableSource, version);
+ }
+ }
+ }
+
const child = mountChildFibers(
workInProgress,
null,
@@ -2253,6 +2273,14 @@ function updateDehydratedSuspenseComponent(
// but after we've already committed once.
warnIfHydrating();
+ if ((getExecutionContext() & RetryAfterError) !== NoContext) {
+ return retrySuspenseComponentWithoutHydrating(
+ current,
+ workInProgress,
+ renderLanes,
+ );
+ }
+
if ((workInProgress.mode & BlockingMode) === NoMode) {
return retrySuspenseComponentWithoutHydrating(
current,
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
index 0a3a4e5a473..2d2916ac777 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js
@@ -13,6 +13,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber} from './ReactInternalTypes';
import type {FiberRoot} from './ReactInternalTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
+import type {MutableSource} from 'shared/ReactTypes';
import type {
SuspenseState,
SuspenseListRenderState,
@@ -114,6 +115,7 @@ import {
isSuspenseInstancePending,
isSuspenseInstanceFallback,
registerSuspenseInstanceRetry,
+ supportsHydration,
} from './ReactFiberHostConfig';
import type {SuspenseInstance} from './ReactFiberHostConfig';
import {shouldSuspend} from './ReactFiberReconciler';
@@ -179,8 +181,12 @@ import {
renderDidSuspendDelayIfPossible,
markUnprocessedUpdateTime,
getWorkInProgressRoot,
+ getExecutionContext,
+ RetryAfterError,
+ NoContext,
} from './ReactFiberWorkLoop.old';
import {unstable_wrap as Schedule_tracing_wrap} from 'scheduler/tracing';
+import {setWorkInProgressVersion} from './ReactMutableSource.old';
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
@@ -1038,6 +1044,20 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
// be any children to hydrate which is effectively the same thing as
// not hydrating.
+ if (supportsHydration) {
+ const mutableSourceEagerHydrationData =
+ root.mutableSourceEagerHydrationData;
+ if (mutableSourceEagerHydrationData != null) {
+ for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
+ const mutableSource = ((mutableSourceEagerHydrationData[
+ i
+ ]: any): MutableSource);
+ const version = mutableSourceEagerHydrationData[i + 1];
+ setWorkInProgressVersion(mutableSource, version);
+ }
+ }
+ }
+
const child = mountChildFibers(
workInProgress,
null,
@@ -2239,6 +2259,14 @@ function updateDehydratedSuspenseComponent(
// but after we've already committed once.
warnIfHydrating();
+ if ((getExecutionContext() & RetryAfterError) !== NoContext) {
+ return retrySuspenseComponentWithoutHydrating(
+ current,
+ workInProgress,
+ renderExpirationTime,
+ );
+ }
+
if ((workInProgress.mode & BlockingMode) === NoMode) {
return retrySuspenseComponentWithoutHydrating(
current,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index 78992d9d783..6cc795b2ed6 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -49,6 +49,7 @@ import {
findBoundingRects as findBoundingRects_old,
focusWithin as focusWithin_old,
observeVisibleRects as observeVisibleRects_old,
+ registerMutableSourceForHydration as registerMutableSourceForHydration_old,
} from './ReactFiberReconciler.old';
import {
@@ -86,6 +87,7 @@ import {
findBoundingRects as findBoundingRects_new,
focusWithin as focusWithin_new,
observeVisibleRects as observeVisibleRects_new,
+ registerMutableSourceForHydration as registerMutableSourceForHydration_new,
} from './ReactFiberReconciler.new';
export const createContainer = enableNewReconciler
@@ -186,3 +188,7 @@ export const focusWithin = enableNewReconciler
export const observeVisibleRects = enableNewReconciler
? observeVisibleRects_new
: observeVisibleRects_old;
+
+export const registerMutableSourceForHydration = enableNewReconciler
+ ? registerMutableSourceForHydration_new
+ : registerMutableSourceForHydration_old;
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js
index eb3eb6a7602..3d508dec78a 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.new.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js
@@ -88,6 +88,7 @@ import {
findHostInstancesForRefresh,
} from './ReactFiberHotReloading.new';
+export {registerMutableSourceForHydration} from './ReactMutableSource.new';
export {createPortal} from './ReactPortal';
export {
createComponentSelector,
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js
index 8646598564c..68d3fbd206c 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.old.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js
@@ -85,6 +85,7 @@ import {
findHostInstancesForRefresh,
} from './ReactFiberHotReloading.old';
+export {registerMutableSourceForHydration} from './ReactMutableSource.old';
export {createPortal} from './ReactPortal';
export {
createComponentSelector,
diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js
index 96f795cbdf2..d1e481414b3 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.new.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.new.js
@@ -10,7 +10,7 @@
import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
import type {RootTag} from './ReactRootTags';
-import {noTimeout} from './ReactFiberHostConfig';
+import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber.new';
import {
NoLanes,
@@ -46,12 +46,15 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.pingedLanes = NoLanes;
this.expiredLanes = NoLanes;
this.mutableReadLanes = NoLanes;
-
this.finishedLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
+ if (supportsHydration) {
+ this.mutableSourceEagerHydrationData = null;
+ }
+
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js
index 72d03a59692..fab9d3def65 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.old.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.old.js
@@ -11,7 +11,7 @@ import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
import type {RootTag} from './ReactRootTags';
-import {noTimeout} from './ReactFiberHostConfig';
+import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber.old';
import {NoWork} from './ReactFiberExpirationTime.old';
import {
@@ -46,6 +46,10 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.lastExpiredTime = NoWork;
this.mutableSourceLastPendingUpdateTime = NoWork;
+ if (supportsHydration) {
+ this.mutableSourceEagerHydrationData = null;
+ }
+
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
index 53ac747aa46..312b74f75b2 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
@@ -62,6 +62,7 @@ import {
warnsIfNotActing,
beforeActiveInstanceBlur,
afterActiveInstanceBlur,
+ clearContainer,
} from './ReactFiberHostConfig';
import {
@@ -215,13 +216,14 @@ const {
type ExecutionContext = number;
-const NoContext = /* */ 0b000000;
-const BatchedContext = /* */ 0b000001;
-const EventContext = /* */ 0b000010;
-const DiscreteEventContext = /* */ 0b000100;
-const LegacyUnbatchedContext = /* */ 0b001000;
-const RenderContext = /* */ 0b010000;
-const CommitContext = /* */ 0b100000;
+export const NoContext = /* */ 0b0000000;
+const BatchedContext = /* */ 0b0000001;
+const EventContext = /* */ 0b0000010;
+const DiscreteEventContext = /* */ 0b0000100;
+const LegacyUnbatchedContext = /* */ 0b0001000;
+const RenderContext = /* */ 0b0010000;
+const CommitContext = /* */ 0b0100000;
+export const RetryAfterError = /* */ 0b1000000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
const RootIncomplete = 0;
@@ -724,6 +726,15 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
prepareFreshStack(root, NoLanes);
} else if (exitStatus !== RootIncomplete) {
if (exitStatus === RootErrored) {
+ executionContext |= RetryAfterError;
+
+ // If an error occurred during hydration,
+ // discard server response and fall back to client side render.
+ if (root.hydrate) {
+ root.hydrate = false;
+ clearContainer(root.containerInfo);
+ }
+
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
@@ -976,6 +987,15 @@ function performSyncWorkOnRoot(root) {
}
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
+ executionContext |= RetryAfterError;
+
+ // If an error occurred during hydration,
+ // discard server response and fall back to client side render.
+ if (root.hydrate) {
+ root.hydrate = false;
+ clearContainer(root.containerInfo);
+ }
+
// If something threw an error, try rendering one more time. We'll render
// synchronously to block concurrent data mutations, and we'll includes
// all pending updates are included. If it still fails after the second
@@ -1016,6 +1036,10 @@ export function flushRoot(root: FiberRoot, lanes: Lanes) {
}
}
+export function getExecutionContext(): ExecutionContext {
+ return executionContext;
+}
+
export function flushDiscreteUpdates() {
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
// However, `act` uses `batchedUpdates`, so there's no way to distinguish
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
index b984a3fa66b..f8dd5185f15 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
@@ -74,6 +74,7 @@ import {
warnsIfNotActing,
beforeActiveInstanceBlur,
afterActiveInstanceBlur,
+ clearContainer,
} from './ReactFiberHostConfig';
import {
@@ -207,13 +208,14 @@ const {
type ExecutionContext = number;
-const NoContext = /* */ 0b000000;
-const BatchedContext = /* */ 0b000001;
-const EventContext = /* */ 0b000010;
-const DiscreteEventContext = /* */ 0b000100;
-const LegacyUnbatchedContext = /* */ 0b001000;
-const RenderContext = /* */ 0b010000;
-const CommitContext = /* */ 0b100000;
+export const NoContext = /* */ 0b0000000;
+const BatchedContext = /* */ 0b0000001;
+const EventContext = /* */ 0b0000010;
+const DiscreteEventContext = /* */ 0b0000100;
+const LegacyUnbatchedContext = /* */ 0b0001000;
+const RenderContext = /* */ 0b0010000;
+const CommitContext = /* */ 0b0100000;
+export const RetryAfterError = /* */ 0b1000000;
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5;
const RootIncomplete = 0;
@@ -728,6 +730,15 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
if (exitStatus !== RootIncomplete) {
if (exitStatus === RootErrored) {
+ executionContext |= RetryAfterError;
+
+ // If an error occurred during hydration,
+ // discard server response and fall back to client side render.
+ if (root.hydrate) {
+ root.hydrate = false;
+ clearContainer(root.containerInfo);
+ }
+
// If something threw an error, try rendering one more time. We'll
// render synchronously to block concurrent data mutations, and we'll
// render at Idle (or lower) so that all pending updates are included.
@@ -1011,6 +1022,15 @@ function performSyncWorkOnRoot(root) {
let exitStatus = renderRootSync(root, expirationTime);
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
+ executionContext |= RetryAfterError;
+
+ // If an error occurred during hydration,
+ // discard server response and fall back to client side render.
+ if (root.hydrate) {
+ root.hydrate = false;
+ clearContainer(root.containerInfo);
+ }
+
// If something threw an error, try rendering one more time. We'll
// render synchronously to block concurrent data mutations, and we'll
// render at Idle (or lower) so that all pending updates are included.
@@ -1051,6 +1071,10 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
}
}
+export function getExecutionContext(): ExecutionContext {
+ return executionContext;
+}
+
export function flushDiscreteUpdates() {
// TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
// However, `act` uses `batchedUpdates`, so there's no way to distinguish
diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js
index 54b3825fa3e..b4374af8e28 100644
--- a/packages/react-reconciler/src/ReactInternalTypes.js
+++ b/packages/react-reconciler/src/ReactInternalTypes.js
@@ -16,6 +16,7 @@ import type {
ReactContext,
MutableSourceSubscribeFn,
MutableSourceGetSnapshotFn,
+ MutableSourceVersion,
MutableSource,
} from 'shared/ReactTypes';
import type {SuspenseInstance} from './ReactFiberHostConfig';
@@ -247,6 +248,11 @@ type BaseFiberRootProperties = {|
// when external, mutable sources are read from during render.
mutableSourceLastPendingUpdateTime: ExpirationTime,
+ // Used by useMutableSource hook to avoid tearing during hydrtaion.
+ mutableSourceEagerHydrationData?: Array<
+ MutableSource | MutableSourceVersion,
+ > | null,
+
// Only used by new reconciler
// Represents the next task that the root should work on, or the current one
diff --git a/packages/react-reconciler/src/ReactMutableSource.new.js b/packages/react-reconciler/src/ReactMutableSource.new.js
index dd21b95781c..61809d33f80 100644
--- a/packages/react-reconciler/src/ReactMutableSource.new.js
+++ b/packages/react-reconciler/src/ReactMutableSource.new.js
@@ -8,6 +8,7 @@
*/
import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes';
+import type {FiberRoot} from './ReactInternalTypes';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
@@ -85,3 +86,23 @@ export function warnAboutMultipleRenderersDEV(
}
}
}
+
+// Eager reads the version of a mutable source and stores it on the root.
+// This ensures that the version used for server rendering matches the one
+// that is eventually read during hydration.
+// If they don't match there's a potential tear and a full deopt render is required.
+export function registerMutableSourceForHydration(
+ root: FiberRoot,
+ mutableSource: MutableSource,
+): void {
+ const getVersion = mutableSource._getVersion;
+ const version = getVersion(mutableSource._source);
+
+ // TODO Clear this data once all pending hydration work is finished.
+ // Retaining it forever may interfere with GC.
+ if (root.mutableSourceEagerHydrationData == null) {
+ root.mutableSourceEagerHydrationData = [mutableSource, version];
+ } else {
+ root.mutableSourceEagerHydrationData.push(mutableSource, version);
+ }
+}
diff --git a/packages/react-reconciler/src/ReactMutableSource.old.js b/packages/react-reconciler/src/ReactMutableSource.old.js
index 6d88953128e..09af3f760f0 100644
--- a/packages/react-reconciler/src/ReactMutableSource.old.js
+++ b/packages/react-reconciler/src/ReactMutableSource.old.js
@@ -116,3 +116,23 @@ export function warnAboutMultipleRenderersDEV(
}
}
}
+
+// Eager reads the version of a mutable source and stores it on the root.
+// This ensures that the version used for server rendering matches the one
+// that is eventually read during hydration.
+// If they don't match there's a potential tear and a full deopt render is required.
+export function registerMutableSourceForHydration(
+ root: FiberRoot,
+ mutableSource: MutableSource,
+): void {
+ const getVersion = mutableSource._getVersion;
+ const version = getVersion(mutableSource._source);
+
+ // TODO Clear this data once all pending hydration work is finished.
+ // Retaining it forever may interfere with GC.
+ if (root.mutableSourceEagerHydrationData == null) {
+ root.mutableSourceEagerHydrationData = [mutableSource, version];
+ } else {
+ root.mutableSourceEagerHydrationData.push(mutableSource, version);
+ }
+}
diff --git a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
new file mode 100644
index 00000000000..e0563e04350
--- /dev/null
+++ b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
@@ -0,0 +1,396 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ * @emails react-core
+ */
+
+'use strict';
+
+let React;
+let ReactDOM;
+let ReactDOMServer;
+let Scheduler;
+let act;
+let useMutableSource;
+
+describe('useMutableSourceHydration', () => {
+ beforeEach(() => {
+ jest.resetModules();
+
+ React = require('react');
+ ReactDOM = require('react-dom');
+ ReactDOMServer = require('react-dom/server');
+ Scheduler = require('scheduler');
+
+ useMutableSource = React.useMutableSource;
+ act = require('react-dom/test-utils').act;
+ });
+
+ const defaultGetSnapshot = source => source.value;
+ const defaultSubscribe = (source, callback) => source.subscribe(callback);
+
+ function createComplexSource(initialValueA, initialValueB) {
+ const callbacksA = [];
+ const callbacksB = [];
+ let revision = 0;
+ let valueA = initialValueA;
+ let valueB = initialValueB;
+
+ const subscribeHelper = (callbacks, callback) => {
+ if (callbacks.indexOf(callback) < 0) {
+ callbacks.push(callback);
+ }
+ return () => {
+ const index = callbacks.indexOf(callback);
+ if (index >= 0) {
+ callbacks.splice(index, 1);
+ }
+ };
+ };
+
+ return {
+ subscribeA(callback) {
+ return subscribeHelper(callbacksA, callback);
+ },
+ subscribeB(callback) {
+ return subscribeHelper(callbacksB, callback);
+ },
+
+ get listenerCountA() {
+ return callbacksA.length;
+ },
+ get listenerCountB() {
+ return callbacksB.length;
+ },
+
+ set valueA(newValue) {
+ revision++;
+ valueA = newValue;
+ callbacksA.forEach(callback => callback());
+ },
+ get valueA() {
+ return valueA;
+ },
+
+ set valueB(newValue) {
+ revision++;
+ valueB = newValue;
+ callbacksB.forEach(callback => callback());
+ },
+ get valueB() {
+ return valueB;
+ },
+
+ get version() {
+ return revision;
+ },
+ };
+ }
+
+ function createSource(initialValue) {
+ const callbacks = [];
+ let revision = 0;
+ let value = initialValue;
+ return {
+ subscribe(callback) {
+ if (callbacks.indexOf(callback) < 0) {
+ callbacks.push(callback);
+ }
+ return () => {
+ const index = callbacks.indexOf(callback);
+ if (index >= 0) {
+ callbacks.splice(index, 1);
+ }
+ };
+ },
+ get listenerCount() {
+ return callbacks.length;
+ },
+ set value(newValue) {
+ revision++;
+ value = newValue;
+ callbacks.forEach(callback => callback());
+ },
+ get value() {
+ return value;
+ },
+ get version() {
+ return revision;
+ },
+ };
+ }
+
+ function createMutableSource(source) {
+ return React.createMutableSource(source, param => param.version);
+ }
+
+ function Component({getSnapshot, label, mutableSource, subscribe}) {
+ const snapshot = useMutableSource(mutableSource, getSnapshot, subscribe);
+ Scheduler.unstable_yieldValue(`${label}:${snapshot}`);
+ return {`${label}:${snapshot}`}
;
+ }
+
+ // @gate experimental
+ it('should render and hydrate', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ function TestComponent() {
+ return (
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const htmlString = ReactDOMServer.renderToString();
+ container.innerHTML = htmlString;
+ expect(Scheduler).toHaveYielded(['only:one']);
+ expect(source.listenerCount).toBe(0);
+
+ const root = ReactDOM.unstable_createRoot(container, {
+ hydrate: true,
+ hydrationOptions: {
+ mutableSources: [mutableSource],
+ },
+ });
+ act(() => {
+ root.render();
+ });
+ expect(Scheduler).toHaveYielded(['only:one']);
+ expect(source.listenerCount).toBe(1);
+ });
+
+ // @gate experimental
+ it('should detect a tear before hydrating a component', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ function TestComponent() {
+ return (
+
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const htmlString = ReactDOMServer.renderToString();
+ container.innerHTML = htmlString;
+ expect(Scheduler).toHaveYielded(['only:one']);
+ expect(source.listenerCount).toBe(0);
+
+ const root = ReactDOM.unstable_createRoot(container, {
+ hydrate: true,
+ hydrationOptions: {
+ mutableSources: [mutableSource],
+ },
+ });
+ expect(() => {
+ act(() => {
+ root.render();
+
+ source.value = 'two';
+ });
+ }).toErrorDev(
+ 'Warning: Did not expect server HTML to contain a in
.',
+ {withoutStack: true},
+ );
+ expect(Scheduler).toHaveYielded(['only:two']);
+ expect(source.listenerCount).toBe(1);
+ });
+
+ // @gate experimental
+ it('should detect a tear between hydrating components', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ function TestComponent() {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const htmlString = ReactDOMServer.renderToString(
);
+ container.innerHTML = htmlString;
+ expect(Scheduler).toHaveYielded(['a:one', 'b:one']);
+ expect(source.listenerCount).toBe(0);
+
+ const root = ReactDOM.unstable_createRoot(container, {
+ hydrate: true,
+ hydrationOptions: {
+ mutableSources: [mutableSource],
+ },
+ });
+ expect(() => {
+ act(() => {
+ root.render(
);
+ expect(Scheduler).toFlushAndYieldThrough(['a:one']);
+ source.value = 'two';
+ });
+ }).toErrorDev(
+ 'Warning: Did not expect server HTML to contain a
in
.',
+ {withoutStack: true},
+ );
+ expect(Scheduler).toHaveYielded(['a:two', 'b:two']);
+ expect(source.listenerCount).toBe(2);
+ });
+
+ // @gate experimental
+ it('should detect a tear between hydrating components reading from different parts of a source', () => {
+ const source = createComplexSource('a:one', 'b:one');
+ const mutableSource = createMutableSource(source);
+
+ // Subscribe to part of the store.
+ const getSnapshotA = s => s.valueA;
+ const subscribeA = (s, callback) => s.subscribeA(callback);
+ const getSnapshotB = s => s.valueB;
+ const subscribeB = (s, callback) => s.subscribeB(callback);
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const htmlString = ReactDOMServer.renderToString(
+ <>
+
+
+ >,
+ );
+ container.innerHTML = htmlString;
+ expect(Scheduler).toHaveYielded(['0:a:one', '1:b:one']);
+
+ const root = ReactDOM.unstable_createRoot(container, {
+ hydrate: true,
+ hydrationOptions: {
+ mutableSources: [mutableSource],
+ },
+ });
+ expect(() => {
+ act(() => {
+ root.render(
+ <>
+
+
+ >,
+ );
+ expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
+ source.valueB = 'b:two';
+ });
+ }).toErrorDev(
+ 'Warning: Did not expect server HTML to contain a
in
.',
+ {withoutStack: true},
+ );
+ expect(Scheduler).toHaveYielded(['0:a:one', '1:b:two']);
+ });
+
+ // @gate experimental
+ it('should detect a tear during a higher priority interruption', () => {
+ const source = createSource('one');
+ const mutableSource = createMutableSource(source);
+
+ function Unrelated({flag}) {
+ Scheduler.unstable_yieldValue(flag);
+ return flag;
+ }
+
+ function TestComponent({flag}) {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ const container = document.createElement('div');
+ document.body.appendChild(container);
+
+ const htmlString = ReactDOMServer.renderToString(
+ ,
+ );
+ container.innerHTML = htmlString;
+ expect(Scheduler).toHaveYielded([1, 'a:one']);
+ expect(source.listenerCount).toBe(0);
+
+ const root = ReactDOM.unstable_createRoot(container, {
+ hydrate: true,
+ hydrationOptions: {
+ mutableSources: [mutableSource],
+ },
+ });
+ expect(() => {
+ act(() => {
+ root.render();
+ expect(Scheduler).toFlushAndYieldThrough([1]);
+
+ // Render an update which will be higher priority than the hydration.
+ Scheduler.unstable_runWithPriority(
+ Scheduler.unstable_UserBlockingPriority,
+ () => root.render(),
+ );
+ expect(Scheduler).toFlushAndYieldThrough([2]);
+
+ source.value = 'two';
+ });
+ }).toErrorDev(
+ 'Warning: Text content did not match. Server: "1" Client: "2"',
+ );
+ expect(Scheduler).toHaveYielded([2, 'a:two']);
+ expect(source.listenerCount).toBe(1);
+ });
+});