diff --git a/packages/react-art/src/ReactART.js b/packages/react-art/src/ReactART.js
index bf97e65031ffa..9b2c5382e1db3 100644
--- a/packages/react-art/src/ReactART.js
+++ b/packages/react-art/src/ReactART.js
@@ -66,7 +66,7 @@ class Surface extends React.Component {
this._surface = Mode.Surface(+width, +height, this._tagRef);
- this._mountNode = createContainer(this._surface, LegacyRoot, false);
+ this._mountNode = createContainer(this._surface, LegacyRoot, false, null);
updateContainer(this.props.children, this._mountNode, this);
}
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index 96381177b487e..11c6ad904b9b1 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -24,6 +24,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableSuspenseServerRenderer = true;
+ ReactFeatureFlags.enableSuspenseCallback = true;
React = require('react');
ReactDOM = require('react-dom');
@@ -92,6 +93,153 @@ describe('ReactDOMServerPartialHydration', () => {
expect(ref.current).toBe(span);
});
+ it('calls the hydration callbacks after hydration or deletion', async () => {
+ let suspend = false;
+ let resolve;
+ let promise = new Promise(resolvePromise => (resolve = resolvePromise));
+ function Child() {
+ if (suspend) {
+ throw promise;
+ } else {
+ return 'Hello';
+ }
+ }
+
+ let suspend2 = false;
+ let promise2 = new Promise(() => {});
+ function Child2() {
+ if (suspend2) {
+ throw promise2;
+ } else {
+ return 'World';
+ }
+ }
+
+ function App({value}) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ // First we render the final HTML. With the streaming renderer
+ // this may have suspense points on the server but here we want
+ // to test the completed HTML. Don't suspend on the server.
+ suspend = false;
+ suspend2 = false;
+ let finalHTML = ReactDOMServer.renderToString();
+
+ let container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ let hydrated = [];
+ let deleted = [];
+
+ // On the client we don't have all data yet but we want to start
+ // hydrating anyway.
+ suspend = true;
+ suspend2 = true;
+ let root = ReactDOM.unstable_createRoot(container, {
+ hydrate: true,
+ hydrationOptions: {
+ onHydrated(node) {
+ hydrated.push(node);
+ },
+ onDeleted(node) {
+ deleted.push(node);
+ },
+ },
+ });
+ act(() => {
+ root.render();
+ });
+
+ expect(hydrated.length).toBe(0);
+ expect(deleted.length).toBe(0);
+
+ await act(async () => {
+ // Resolving the promise should continue hydration
+ suspend = false;
+ resolve();
+ await promise;
+ });
+
+ expect(hydrated.length).toBe(1);
+ expect(deleted.length).toBe(0);
+
+ // Performing an update should force it to delete the boundary
+ root.render();
+
+ Scheduler.unstable_flushAll();
+ jest.runAllTimers();
+
+ expect(hydrated.length).toBe(1);
+ expect(deleted.length).toBe(1);
+ });
+
+ it('calls the onDeleted hydration callback if the parent gets deleted', async () => {
+ let suspend = false;
+ let promise = new Promise(() => {});
+ function Child() {
+ if (suspend) {
+ throw promise;
+ } else {
+ return 'Hello';
+ }
+ }
+
+ function App({deleted}) {
+ if (deleted) {
+ return null;
+ }
+ return (
+
+
+
+
+
+ );
+ }
+
+ suspend = false;
+ let finalHTML = ReactDOMServer.renderToString();
+
+ let container = document.createElement('div');
+ container.innerHTML = finalHTML;
+
+ let deleted = [];
+
+ // On the client we don't have all data yet but we want to start
+ // hydrating anyway.
+ suspend = true;
+ let root = ReactDOM.unstable_createRoot(container, {
+ hydrate: true,
+ hydrationOptions: {
+ onDeleted(node) {
+ deleted.push(node);
+ },
+ },
+ });
+ act(() => {
+ root.render();
+ });
+
+ expect(deleted.length).toBe(0);
+
+ act(() => {
+ root.render();
+ });
+
+ // The callback should have been invoked.
+ expect(deleted.length).toBe(1);
+ });
+
it('warns and replaces the boundary content in legacy mode', async () => {
let suspend = false;
let resolve;
diff --git a/packages/react-dom/src/client/ReactDOM.js b/packages/react-dom/src/client/ReactDOM.js
index 2e3ccea8de18f..e11756cb6bf88 100644
--- a/packages/react-dom/src/client/ReactDOM.js
+++ b/packages/react-dom/src/client/ReactDOM.js
@@ -367,15 +367,26 @@ ReactWork.prototype._onCommit = function(): void {
function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
- hydrate: boolean,
+ options: void | RootOptions,
) {
// Tag is either LegacyRoot or Concurrent Root
- const root = createContainer(container, tag, hydrate);
+ const hydrate = options != null && options.hydrate === true;
+ const hydrationCallbacks =
+ (options != null && options.hydrationOptions) || null;
+ const root = createContainer(container, tag, hydrate, hydrationCallbacks);
this._internalRoot = root;
}
-function ReactRoot(container: DOMContainer, hydrate: boolean) {
- const root = createContainer(container, ConcurrentRoot, hydrate);
+function ReactRoot(container: DOMContainer, options: void | RootOptions) {
+ const hydrate = options != null && options.hydrate === true;
+ const hydrationCallbacks =
+ (options != null && options.hydrationOptions) || null;
+ const root = createContainer(
+ container,
+ ConcurrentRoot,
+ hydrate,
+ hydrationCallbacks,
+ );
this._internalRoot = root;
}
@@ -532,7 +543,15 @@ function legacyCreateRootFromDOMContainer(
}
// Legacy roots are not batched.
- return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
+ return new ReactSyncRoot(
+ container,
+ LegacyRoot,
+ shouldHydrate
+ ? {
+ hydrate: true,
+ }
+ : undefined,
+ );
}
function legacyRenderSubtreeIntoContainer(
@@ -824,6 +843,10 @@ const ReactDOM: Object = {
type RootOptions = {
hydrate?: boolean,
+ hydrationOptions?: {
+ onHydrated?: (suspenseNode: Comment) => void,
+ onDeleted?: (suspenseNode: Comment) => void,
+ },
};
function createRoot(
@@ -839,8 +862,7 @@ function createRoot(
functionName,
);
warnIfReactDOMContainerInDEV(container);
- const hydrate = options != null && options.hydrate === true;
- return new ReactRoot(container, hydrate);
+ return new ReactRoot(container, options);
}
function createSyncRoot(
@@ -856,8 +878,7 @@ function createSyncRoot(
functionName,
);
warnIfReactDOMContainerInDEV(container);
- const hydrate = options != null && options.hydrate === true;
- return new ReactSyncRoot(container, BatchedRoot, hydrate);
+ return new ReactSyncRoot(container, BatchedRoot, options);
}
function warnIfReactDOMContainerInDEV(container) {
diff --git a/packages/react-native-renderer/src/ReactFabric.js b/packages/react-native-renderer/src/ReactFabric.js
index 4637f9a0bc80d..72f7a46098191 100644
--- a/packages/react-native-renderer/src/ReactFabric.js
+++ b/packages/react-native-renderer/src/ReactFabric.js
@@ -144,7 +144,7 @@ const ReactFabric: ReactFabricType = {
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
- root = createContainer(containerTag, LegacyRoot, false);
+ root = createContainer(containerTag, LegacyRoot, false, null);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
diff --git a/packages/react-native-renderer/src/ReactNativeRenderer.js b/packages/react-native-renderer/src/ReactNativeRenderer.js
index 853c5f54fc7ae..21ec4e239e2cd 100644
--- a/packages/react-native-renderer/src/ReactNativeRenderer.js
+++ b/packages/react-native-renderer/src/ReactNativeRenderer.js
@@ -141,7 +141,7 @@ const ReactNativeRenderer: ReactNativeType = {
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
- root = createContainer(containerTag, LegacyRoot, false);
+ root = createContainer(containerTag, LegacyRoot, false, null);
roots.set(containerTag, root);
}
updateContainer(element, root, null, callback);
diff --git a/packages/react-noop-renderer/src/createReactNoop.js b/packages/react-noop-renderer/src/createReactNoop.js
index 7369552eb34e6..1ae8275449cc5 100644
--- a/packages/react-noop-renderer/src/createReactNoop.js
+++ b/packages/react-noop-renderer/src/createReactNoop.js
@@ -908,7 +908,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
if (!root) {
const container = {rootID: rootID, pendingChildren: [], children: []};
rootContainers.set(rootID, container);
- root = NoopRenderer.createContainer(container, tag, false);
+ root = NoopRenderer.createContainer(container, tag, false, null);
roots.set(rootID, root);
}
return root.current.stateNode.containerInfo;
@@ -925,6 +925,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
container,
ConcurrentRoot,
false,
+ null,
);
return {
_Scheduler: Scheduler,
@@ -950,6 +951,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
container,
BatchedRoot,
false,
+ null,
);
return {
_Scheduler: Scheduler,
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 3a6ff1f5aca05..b8996ad698ee1 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -607,7 +607,12 @@ function commitLifeCycles(
}
return;
}
- case SuspenseComponent:
+ case SuspenseComponent: {
+ if (enableSuspenseCallback) {
+ commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
+ }
+ return;
+ }
case SuspenseListComponent:
case IncompleteClassComponent:
case FundamentalComponent:
@@ -644,7 +649,8 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
}
} else if (
node.tag === SuspenseComponent &&
- node.memoizedState !== null
+ node.memoizedState !== null &&
+ node.memoizedState.dehydrated === null
) {
// Found a nested Suspense component that timed out. Skip over the
// primary child fragment, which should remain hidden.
@@ -719,6 +725,7 @@ function commitDetachRef(current: Fiber) {
// deletion, so don't let them throw. Host-originating errors should
// interrupt deletion, so it's okay
function commitUnmount(
+ finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
@@ -801,7 +808,7 @@ function commitUnmount(
// We are also not using this parent because
// the portal will get pushed immediately.
if (supportsMutation) {
- unmountHostComponents(current, renderPriorityLevel);
+ unmountHostComponents(finishedRoot, current, renderPriorityLevel);
} else if (supportsPersistence) {
emptyPortalContainer(current);
}
@@ -815,11 +822,24 @@ function commitUnmount(
current.stateNode = null;
}
}
+ return;
+ }
+ case DehydratedFragment: {
+ if (enableSuspenseCallback) {
+ const hydrationCallbacks = finishedRoot.hydrationCallbacks;
+ if (hydrationCallbacks !== null) {
+ const onDeleted = hydrationCallbacks.onDeleted;
+ if (onDeleted) {
+ onDeleted((current.stateNode: SuspenseInstance));
+ }
+ }
+ }
}
}
}
function commitNestedUnmounts(
+ finishedRoot: FiberRoot,
root: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
@@ -830,7 +850,7 @@ function commitNestedUnmounts(
// we do an inner loop while we're still inside the host node.
let node: Fiber = root;
while (true) {
- commitUnmount(node, renderPriorityLevel);
+ commitUnmount(finishedRoot, node, renderPriorityLevel);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
if (
@@ -1081,7 +1101,11 @@ function commitPlacement(finishedWork: Fiber): void {
}
}
-function unmountHostComponents(current, renderPriorityLevel): void {
+function unmountHostComponents(
+ finishedRoot,
+ current,
+ renderPriorityLevel,
+): void {
// We only have the top Fiber that was deleted but we need to recurse down its
// children to find all the terminal nodes.
let node: Fiber = current;
@@ -1129,7 +1153,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
}
if (node.tag === HostComponent || node.tag === HostText) {
- commitNestedUnmounts(node, renderPriorityLevel);
+ commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
@@ -1146,7 +1170,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
// Don't visit children because we already visited them.
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
const fundamentalNode = node.stateNode.instance;
- commitNestedUnmounts(node, renderPriorityLevel);
+ commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
@@ -1164,6 +1188,16 @@ function unmountHostComponents(current, renderPriorityLevel): void {
enableSuspenseServerRenderer &&
node.tag === DehydratedFragment
) {
+ if (enableSuspenseCallback) {
+ const hydrationCallbacks = finishedRoot.hydrationCallbacks;
+ if (hydrationCallbacks !== null) {
+ const onDeleted = hydrationCallbacks.onDeleted;
+ if (onDeleted) {
+ onDeleted((node.stateNode: SuspenseInstance));
+ }
+ }
+ }
+
// Delete the dehydrated suspense boundary and all of its content.
if (currentParentIsContainer) {
clearSuspenseBoundaryFromContainer(
@@ -1188,7 +1222,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
continue;
}
} else {
- commitUnmount(node, renderPriorityLevel);
+ commitUnmount(finishedRoot, node, renderPriorityLevel);
// Visit children because we may find more host components below.
if (node.child !== null) {
node.child.return = node;
@@ -1216,16 +1250,17 @@ function unmountHostComponents(current, renderPriorityLevel): void {
}
function commitDeletion(
+ finishedRoot: FiberRoot,
current: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
if (supportsMutation) {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
- unmountHostComponents(current, renderPriorityLevel);
+ unmountHostComponents(finishedRoot, current, renderPriorityLevel);
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
- commitNestedUnmounts(current, renderPriorityLevel);
+ commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
}
detachFiber(current);
}
@@ -1382,6 +1417,30 @@ function commitSuspenseComponent(finishedWork: Fiber) {
}
}
+function commitSuspenseHydrationCallbacks(
+ finishedRoot: FiberRoot,
+ finishedWork: Fiber,
+) {
+ if (enableSuspenseCallback) {
+ const hydrationCallbacks = finishedRoot.hydrationCallbacks;
+ if (hydrationCallbacks !== null) {
+ const onHydrated = hydrationCallbacks.onHydrated;
+ if (onHydrated) {
+ const newState: SuspenseState | null = finishedWork.memoizedState;
+ if (newState === null) {
+ const current = finishedWork.alternate;
+ if (current !== null) {
+ const prevState: SuspenseState | null = current.memoizedState;
+ if (prevState !== null && prevState.dehydrated !== null) {
+ onHydrated(prevState.dehydrated);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
function attachSuspenseRetryListeners(finishedWork: Fiber) {
// If this boundary just timed out, then it will have a set of thenables.
// For each thenable, attach a listener so that when it resolves, React
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js
index 6433a193ea69b..6d5fa4f3173fb 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js
@@ -859,6 +859,10 @@ function completeWork(
if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// This boundary did not suspend so it's now hydrated and unsuspended.
workInProgress.memoizedState = null;
+ if (enableSuspenseCallback) {
+ // Notify the callback.
+ workInProgress.effectTag |= Update;
+ }
} else {
// Something suspended. Schedule an effect to attach retry listeners.
workInProgress.effectTag |= Update;
diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js
index 81c8418263d65..03378ee54a234 100644
--- a/packages/react-reconciler/src/ReactFiberReconciler.js
+++ b/packages/react-reconciler/src/ReactFiberReconciler.js
@@ -20,6 +20,7 @@ import {FundamentalComponent} from 'shared/ReactWorkTags';
import type {ReactNodeList} from 'shared/ReactTypes';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
+import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
import {
findCurrentHostFiber,
@@ -294,8 +295,9 @@ export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
+ hydrationCallbacks: null | SuspenseHydrationCallbacks,
): OpaqueRoot {
- return createFiberRoot(containerInfo, tag, hydrate);
+ return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
}
export function updateContainer(
diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js
index 148ff8a8e9a69..7ef93739039e0 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.js
@@ -13,11 +13,15 @@ import type {RootTag} from 'shared/ReactRootTags';
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
import type {Thenable} from './ReactFiberWorkLoop';
import type {Interaction} from 'scheduler/src/Tracing';
+import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
import {noTimeout} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber';
import {NoWork} from './ReactFiberExpirationTime';
-import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
+import {
+ enableSchedulerTracing,
+ enableSuspenseCallback,
+} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
// TODO: This should be lifted into the renderer.
@@ -83,6 +87,11 @@ type ProfilingOnlyFiberRootProperties = {|
pendingInteractionMap: PendingInteractionMap,
|};
+// The follow fields are only used by enableSuspenseCallback for hydration.
+type SuspenseCallbackOnlyFiberRootProperties = {|
+ hydrationCallbacks: null | SuspenseHydrationCallbacks,
+|};
+
// Exported FiberRoot type includes all properties,
// To avoid requiring potentially error-prone :any casts throughout the project.
// Profiling properties are only safe to access in profiling builds (when enableSchedulerTracing is true).
@@ -91,6 +100,7 @@ type ProfilingOnlyFiberRootProperties = {|
export type FiberRoot = {
...BaseFiberRootProperties,
...ProfilingOnlyFiberRootProperties,
+ ...SuspenseCallbackOnlyFiberRootProperties,
};
function FiberRootNode(containerInfo, tag, hydrate) {
@@ -117,14 +127,21 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.memoizedInteractions = new Set();
this.pendingInteractionMap = new Map();
}
+ if (enableSuspenseCallback) {
+ this.hydrationCallbacks = null;
+ }
}
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
+ hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
+ if (enableSuspenseCallback) {
+ root.hydrationCallbacks = hydrationCallbacks;
+ }
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js
index 284c6f44291f5..dfb6964d64e28 100644
--- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js
@@ -17,6 +17,11 @@ import {
isSuspenseInstanceFallback,
} from './ReactFiberHostConfig';
+export type SuspenseHydrationCallbacks = {
+ onHydrated?: (suspenseInstance: SuspenseInstance) => void,
+ onDeleted?: (suspenseInstance: SuspenseInstance) => void,
+};
+
// A null SuspenseState represents an unsuspended normal Suspense boundary.
// A non-null SuspenseState means that it is blocked for one reason or another.
// - A non-null dehydrated field means it's blocked pending hydration.
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index efbef396d0315..21454b8feebac 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -1638,6 +1638,7 @@ function commitRootImpl(root, renderPriorityLevel) {
null,
commitMutationEffects,
null,
+ root,
renderPriorityLevel,
);
if (hasCaughtError()) {
@@ -1648,7 +1649,7 @@ function commitRootImpl(root, renderPriorityLevel) {
}
} else {
try {
- commitMutationEffects(renderPriorityLevel);
+ commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
@@ -1837,7 +1838,7 @@ function commitBeforeMutationEffects() {
}
}
-function commitMutationEffects(renderPriorityLevel) {
+function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// TODO: Should probably move the bulk of this function to commitWork.
while (nextEffect !== null) {
setCurrentDebugFiberInDEV(nextEffect);
@@ -1888,7 +1889,7 @@ function commitMutationEffects(renderPriorityLevel) {
break;
}
case Deletion: {
- commitDeletion(nextEffect, renderPriorityLevel);
+ commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
diff --git a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
index 1849eceaae98a..6a724025abdcb 100644
--- a/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
@@ -58,6 +58,7 @@ describe('ReactFiberHostContext', () => {
/* root: */ null,
ConcurrentRoot,
false,
+ null,
);
Renderer.updateContainer(
@@ -110,6 +111,7 @@ describe('ReactFiberHostContext', () => {
rootContext,
ConcurrentRoot,
false,
+ null,
);
Renderer.updateContainer(
diff --git a/packages/react-test-renderer/src/ReactTestRenderer.js b/packages/react-test-renderer/src/ReactTestRenderer.js
index 11b6fed109697..f5133c528464f 100644
--- a/packages/react-test-renderer/src/ReactTestRenderer.js
+++ b/packages/react-test-renderer/src/ReactTestRenderer.js
@@ -442,6 +442,7 @@ const ReactTestRendererFiber = {
container,
isConcurrent ? ConcurrentRoot : LegacyRoot,
false,
+ null,
);
invariant(root != null, 'something went wrong');
updateContainer(element, root, null, null);
diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js
index 34d482cf2b46b..3b64033a6e2ff 100644
--- a/packages/shared/ReactFeatureFlags.js
+++ b/packages/shared/ReactFeatureFlags.js
@@ -84,6 +84,8 @@ export const enableUserBlockingEvents = false;
// Add a callback property to suspense to notify which promises are currently
// in the update queue. This allows reporting and tracing of what is causing
// the user to see a loading state.
+// Also allows hydration callbacks to fire when a dehydrated boundary gets
+// hydrated or deleted.
export const enableSuspenseCallback = false;
// Part of the simplification of React.createElement so we can eventually move