@samitouri / QOS-React-2 / commits / c80678c760

Add "hydrationOptions" behind the enableSuspenseCallback flag (#16434)

This gets invoked when a boundary is either hydrated or if it is deleted because it updated or got deleted before it mounted.

Sebastian Markbåge committed Aug 19, 2019 at 13:26 UTC c80678c7606b1895573c23182bfb9a418e2ad31e
15 files changed +292 -28
packages/react-art/src/ReactART.js
+1 -1
@@ -66,7 +66,7 @@ class Surface extends React.Component {
66
67 this._surface = Mode.Surface(+width, +height, this._tagRef);
68
69 - this._mountNode = createContainer(this._surface, LegacyRoot, false);
69 + this._mountNode = createContainer(this._surface, LegacyRoot, false, null);
70 updateContainer(this.props.children, this._mountNode, this);
71 }
72
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+148
@@ -24,6 +24,7 @@ describe('ReactDOMServerPartialHydration', () => {
24
25 ReactFeatureFlags = require('shared/ReactFeatureFlags');
26 ReactFeatureFlags.enableSuspenseServerRenderer = true;
27 + ReactFeatureFlags.enableSuspenseCallback = true;
28
29 React = require('react');
30 ReactDOM = require('react-dom');
@@ -92,6 +93,153 @@ describe('ReactDOMServerPartialHydration', () => {
93 expect(ref.current).toBe(span);
94 });
95
96 + it('calls the hydration callbacks after hydration or deletion', async () => {
97 + let suspend = false;
98 + let resolve;
99 + let promise = new Promise(resolvePromise => (resolve = resolvePromise));
100 + function Child() {
101 + if (suspend) {
102 + throw promise;
103 + } else {
104 + return 'Hello';
105 + }
106 + }
107 +
108 + let suspend2 = false;
109 + let promise2 = new Promise(() => {});
110 + function Child2() {
111 + if (suspend2) {
112 + throw promise2;
113 + } else {
114 + return 'World';
115 + }
116 + }
117 +
118 + function App({value}) {
119 + return (
120 + <div>
121 + <Suspense fallback="Loading...">
122 + <Child />
123 + </Suspense>
124 + <Suspense fallback="Loading...">
125 + <Child2 value={value} />
126 + </Suspense>
127 + </div>
128 + );
129 + }
130 +
131 + // First we render the final HTML. With the streaming renderer
132 + // this may have suspense points on the server but here we want
133 + // to test the completed HTML. Don't suspend on the server.
134 + suspend = false;
135 + suspend2 = false;
136 + let finalHTML = ReactDOMServer.renderToString(<App />);
137 +
138 + let container = document.createElement('div');
139 + container.innerHTML = finalHTML;
140 +
141 + let hydrated = [];
142 + let deleted = [];
143 +
144 + // On the client we don't have all data yet but we want to start
145 + // hydrating anyway.
146 + suspend = true;
147 + suspend2 = true;
148 + let root = ReactDOM.unstable_createRoot(container, {
149 + hydrate: true,
150 + hydrationOptions: {
151 + onHydrated(node) {
152 + hydrated.push(node);
153 + },
154 + onDeleted(node) {
155 + deleted.push(node);
156 + },
157 + },
158 + });
159 + act(() => {
160 + root.render(<App />);
161 + });
162 +
163 + expect(hydrated.length).toBe(0);
164 + expect(deleted.length).toBe(0);
165 +
166 + await act(async () => {
167 + // Resolving the promise should continue hydration
168 + suspend = false;
169 + resolve();
170 + await promise;
171 + });
172 +
173 + expect(hydrated.length).toBe(1);
174 + expect(deleted.length).toBe(0);
175 +
176 + // Performing an update should force it to delete the boundary
177 + root.render(<App value={true} />);
178 +
179 + Scheduler.unstable_flushAll();
180 + jest.runAllTimers();
181 +
182 + expect(hydrated.length).toBe(1);
183 + expect(deleted.length).toBe(1);
184 + });
185 +
186 + it('calls the onDeleted hydration callback if the parent gets deleted', async () => {
187 + let suspend = false;
188 + let promise = new Promise(() => {});
189 + function Child() {
190 + if (suspend) {
191 + throw promise;
192 + } else {
193 + return 'Hello';
194 + }
195 + }
196 +
197 + function App({deleted}) {
198 + if (deleted) {
199 + return null;
200 + }
201 + return (
202 + <div>
203 + <Suspense fallback="Loading...">
204 + <Child />
205 + </Suspense>
206 + </div>
207 + );
208 + }
209 +
210 + suspend = false;
211 + let finalHTML = ReactDOMServer.renderToString(<App />);
212 +
213 + let container = document.createElement('div');
214 + container.innerHTML = finalHTML;
215 +
216 + let deleted = [];
217 +
218 + // On the client we don't have all data yet but we want to start
219 + // hydrating anyway.
220 + suspend = true;
221 + let root = ReactDOM.unstable_createRoot(container, {
222 + hydrate: true,
223 + hydrationOptions: {
224 + onDeleted(node) {
225 + deleted.push(node);
226 + },
227 + },
228 + });
229 + act(() => {
230 + root.render(<App />);
231 + });
232 +
233 + expect(deleted.length).toBe(0);
234 +
235 + act(() => {
236 + root.render(<App deleted={true} />);
237 + });
238 +
239 + // The callback should have been invoked.
240 + expect(deleted.length).toBe(1);
241 + });
242 +
243 it('warns and replaces the boundary content in legacy mode', async () => {
244 let suspend = false;
245 let resolve;
packages/react-dom/src/client/ReactDOM.js
+30 -9
@@ -367,15 +367,26 @@ ReactWork.prototype._onCommit = function(): void {
367 function ReactSyncRoot(
368 container: DOMContainer,
369 tag: RootTag,
370 - hydrate: boolean,
370 + options: void | RootOptions,
371 ) {
372 // Tag is either LegacyRoot or Concurrent Root
373 - const root = createContainer(container, tag, hydrate);
373 + const hydrate = options != null && options.hydrate === true;
374 + const hydrationCallbacks =
375 + (options != null && options.hydrationOptions) || null;
376 + const root = createContainer(container, tag, hydrate, hydrationCallbacks);
377 this._internalRoot = root;
378 }
379
377 -function ReactRoot(container: DOMContainer, hydrate: boolean) {
378 - const root = createContainer(container, ConcurrentRoot, hydrate);
380 +function ReactRoot(container: DOMContainer, options: void | RootOptions) {
381 + const hydrate = options != null && options.hydrate === true;
382 + const hydrationCallbacks =
383 + (options != null && options.hydrationOptions) || null;
384 + const root = createContainer(
385 + container,
386 + ConcurrentRoot,
387 + hydrate,
388 + hydrationCallbacks,
389 + );
390 this._internalRoot = root;
391 }
392
@@ -532,7 +543,15 @@ function legacyCreateRootFromDOMContainer(
543 }
544
545 // Legacy roots are not batched.
535 - return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
546 + return new ReactSyncRoot(
547 + container,
548 + LegacyRoot,
549 + shouldHydrate
550 + ? {
551 + hydrate: true,
552 + }
553 + : undefined,
554 + );
555 }
556
557 function legacyRenderSubtreeIntoContainer(
@@ -824,6 +843,10 @@ const ReactDOM: Object = {
843
844 type RootOptions = {
845 hydrate?: boolean,
846 + hydrationOptions?: {
847 + onHydrated?: (suspenseNode: Comment) => void,
848 + onDeleted?: (suspenseNode: Comment) => void,
849 + },
850 };
851
852 function createRoot(
@@ -839,8 +862,7 @@ function createRoot(
862 functionName,
863 );
864 warnIfReactDOMContainerInDEV(container);
842 - const hydrate = options != null && options.hydrate === true;
843 - return new ReactRoot(container, hydrate);
865 + return new ReactRoot(container, options);
866 }
867
868 function createSyncRoot(
@@ -856,8 +878,7 @@ function createSyncRoot(
878 functionName,
879 );
880 warnIfReactDOMContainerInDEV(container);
859 - const hydrate = options != null && options.hydrate === true;
860 - return new ReactSyncRoot(container, BatchedRoot, hydrate);
881 + return new ReactSyncRoot(container, BatchedRoot, options);
882 }
883
884 function warnIfReactDOMContainerInDEV(container) {
packages/react-native-renderer/src/ReactFabric.js
+1 -1
@@ -144,7 +144,7 @@ const ReactFabric: ReactFabricType = {
144 if (!root) {
145 // TODO (bvaughn): If we decide to keep the wrapper component,
146 // We could create a wrapper for containerTag as well to reduce special casing.
147 - root = createContainer(containerTag, LegacyRoot, false);
147 + root = createContainer(containerTag, LegacyRoot, false, null);
148 roots.set(containerTag, root);
149 }
150 updateContainer(element, root, null, callback);
packages/react-native-renderer/src/ReactNativeRenderer.js
+1 -1
@@ -141,7 +141,7 @@ const ReactNativeRenderer: ReactNativeType = {
141 if (!root) {
142 // TODO (bvaughn): If we decide to keep the wrapper component,
143 // We could create a wrapper for containerTag as well to reduce special casing.
144 - root = createContainer(containerTag, LegacyRoot, false);
144 + root = createContainer(containerTag, LegacyRoot, false, null);
145 roots.set(containerTag, root);
146 }
147 updateContainer(element, root, null, callback);
packages/react-noop-renderer/src/createReactNoop.js
+3 -1
@@ -908,7 +908,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
908 if (!root) {
909 const container = {rootID: rootID, pendingChildren: [], children: []};
910 rootContainers.set(rootID, container);
911 - root = NoopRenderer.createContainer(container, tag, false);
911 + root = NoopRenderer.createContainer(container, tag, false, null);
912 roots.set(rootID, root);
913 }
914 return root.current.stateNode.containerInfo;
@@ -925,6 +925,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
925 container,
926 ConcurrentRoot,
927 false,
928 + null,
929 );
930 return {
931 _Scheduler: Scheduler,
@@ -950,6 +951,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
951 container,
952 BatchedRoot,
953 false,
954 + null,
955 );
956 return {
957 _Scheduler: Scheduler,
packages/react-reconciler/src/ReactFiberCommitWork.js
+69 -10
@@ -607,7 +607,12 @@ function commitLifeCycles(
607 }
608 return;
609 }
610 - case SuspenseComponent:
610 + case SuspenseComponent: {
611 + if (enableSuspenseCallback) {
612 + commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
613 + }
614 + return;
615 + }
616 case SuspenseListComponent:
617 case IncompleteClassComponent:
618 case FundamentalComponent:
@@ -644,7 +649,8 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
649 }
650 } else if (
651 node.tag === SuspenseComponent &&
647 - node.memoizedState !== null
652 + node.memoizedState !== null &&
653 + node.memoizedState.dehydrated === null
654 ) {
655 // Found a nested Suspense component that timed out. Skip over the
656 // primary child fragment, which should remain hidden.
@@ -719,6 +725,7 @@ function commitDetachRef(current: Fiber) {
725 // deletion, so don't let them throw. Host-originating errors should
726 // interrupt deletion, so it's okay
727 function commitUnmount(
728 + finishedRoot: FiberRoot,
729 current: Fiber,
730 renderPriorityLevel: ReactPriorityLevel,
731 ): void {
@@ -801,7 +808,7 @@ function commitUnmount(
808 // We are also not using this parent because
809 // the portal will get pushed immediately.
810 if (supportsMutation) {
804 - unmountHostComponents(current, renderPriorityLevel);
811 + unmountHostComponents(finishedRoot, current, renderPriorityLevel);
812 } else if (supportsPersistence) {
813 emptyPortalContainer(current);
814 }
@@ -815,11 +822,24 @@ function commitUnmount(
822 current.stateNode = null;
823 }
824 }
825 + return;
826 + }
827 + case DehydratedFragment: {
828 + if (enableSuspenseCallback) {
829 + const hydrationCallbacks = finishedRoot.hydrationCallbacks;
830 + if (hydrationCallbacks !== null) {
831 + const onDeleted = hydrationCallbacks.onDeleted;
832 + if (onDeleted) {
833 + onDeleted((current.stateNode: SuspenseInstance));
834 + }
835 + }
836 + }
837 }
838 }
839 }
840
841 function commitNestedUnmounts(
842 + finishedRoot: FiberRoot,
843 root: Fiber,
844 renderPriorityLevel: ReactPriorityLevel,
845 ): void {
@@ -830,7 +850,7 @@ function commitNestedUnmounts(
850 // we do an inner loop while we're still inside the host node.
851 let node: Fiber = root;
852 while (true) {
833 - commitUnmount(node, renderPriorityLevel);
853 + commitUnmount(finishedRoot, node, renderPriorityLevel);
854 // Visit children because they may contain more composite or host nodes.
855 // Skip portals because commitUnmount() currently visits them recursively.
856 if (
@@ -1081,7 +1101,11 @@ function commitPlacement(finishedWork: Fiber): void {
1101 }
1102 }
1103
1084 -function unmountHostComponents(current, renderPriorityLevel): void {
1104 +function unmountHostComponents(
1105 + finishedRoot,
1106 + current,
1107 + renderPriorityLevel,
1108 +): void {
1109 // We only have the top Fiber that was deleted but we need to recurse down its
1110 // children to find all the terminal nodes.
1111 let node: Fiber = current;
@@ -1129,7 +1153,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
1153 }
1154
1155 if (node.tag === HostComponent || node.tag === HostText) {
1132 - commitNestedUnmounts(node, renderPriorityLevel);
1156 + commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1157 // After all the children have unmounted, it is now safe to remove the
1158 // node from the tree.
1159 if (currentParentIsContainer) {
@@ -1146,7 +1170,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
1170 // Don't visit children because we already visited them.
1171 } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
1172 const fundamentalNode = node.stateNode.instance;
1149 - commitNestedUnmounts(node, renderPriorityLevel);
1173 + commitNestedUnmounts(finishedRoot, node, renderPriorityLevel);
1174 // After all the children have unmounted, it is now safe to remove the
1175 // node from the tree.
1176 if (currentParentIsContainer) {
@@ -1164,6 +1188,16 @@ function unmountHostComponents(current, renderPriorityLevel): void {
1188 enableSuspenseServerRenderer &&
1189 node.tag === DehydratedFragment
1190 ) {
1191 + if (enableSuspenseCallback) {
1192 + const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1193 + if (hydrationCallbacks !== null) {
1194 + const onDeleted = hydrationCallbacks.onDeleted;
1195 + if (onDeleted) {
1196 + onDeleted((node.stateNode: SuspenseInstance));
1197 + }
1198 + }
1199 + }
1200 +
1201 // Delete the dehydrated suspense boundary and all of its content.
1202 if (currentParentIsContainer) {
1203 clearSuspenseBoundaryFromContainer(
@@ -1188,7 +1222,7 @@ function unmountHostComponents(current, renderPriorityLevel): void {
1222 continue;
1223 }
1224 } else {
1191 - commitUnmount(node, renderPriorityLevel);
1225 + commitUnmount(finishedRoot, node, renderPriorityLevel);
1226 // Visit children because we may find more host components below.
1227 if (node.child !== null) {
1228 node.child.return = node;
@@ -1216,16 +1250,17 @@ function unmountHostComponents(current, renderPriorityLevel): void {
1250 }
1251
1252 function commitDeletion(
1253 + finishedRoot: FiberRoot,
1254 current: Fiber,
1255 renderPriorityLevel: ReactPriorityLevel,
1256 ): void {
1257 if (supportsMutation) {
1258 // Recursively delete all host nodes from the parent.
1259 // Detach refs and call componentWillUnmount() on the whole subtree.
1225 - unmountHostComponents(current, renderPriorityLevel);
1260 + unmountHostComponents(finishedRoot, current, renderPriorityLevel);
1261 } else {
1262 // Detach refs and call componentWillUnmount() on the whole subtree.
1228 - commitNestedUnmounts(current, renderPriorityLevel);
1263 + commitNestedUnmounts(finishedRoot, current, renderPriorityLevel);
1264 }
1265 detachFiber(current);
1266 }
@@ -1382,6 +1417,30 @@ function commitSuspenseComponent(finishedWork: Fiber) {
1417 }
1418 }
1419
1420 +function commitSuspenseHydrationCallbacks(
1421 + finishedRoot: FiberRoot,
1422 + finishedWork: Fiber,
1423 +) {
1424 + if (enableSuspenseCallback) {
1425 + const hydrationCallbacks = finishedRoot.hydrationCallbacks;
1426 + if (hydrationCallbacks !== null) {
1427 + const onHydrated = hydrationCallbacks.onHydrated;
1428 + if (onHydrated) {
1429 + const newState: SuspenseState | null = finishedWork.memoizedState;
1430 + if (newState === null) {
1431 + const current = finishedWork.alternate;
1432 + if (current !== null) {
1433 + const prevState: SuspenseState | null = current.memoizedState;
1434 + if (prevState !== null && prevState.dehydrated !== null) {
1435 + onHydrated(prevState.dehydrated);
1436 + }
1437 + }
1438 + }
1439 + }
1440 + }
1441 + }
1442 +}
1443 +
1444 function attachSuspenseRetryListeners(finishedWork: Fiber) {
1445 // If this boundary just timed out, then it will have a set of thenables.
1446 // For each thenable, attach a listener so that when it resolves, React
packages/react-reconciler/src/ReactFiberCompleteWork.js
+4
@@ -859,6 +859,10 @@ function completeWork(
859 if ((workInProgress.effectTag & DidCapture) === NoEffect) {
860 // This boundary did not suspend so it's now hydrated and unsuspended.
861 workInProgress.memoizedState = null;
862 + if (enableSuspenseCallback) {
863 + // Notify the callback.
864 + workInProgress.effectTag |= Update;
865 + }
866 } else {
867 // Something suspended. Schedule an effect to attach retry listeners.
868 workInProgress.effectTag |= Update;
packages/react-reconciler/src/ReactFiberReconciler.js
+3 -1
@@ -20,6 +20,7 @@ import {FundamentalComponent} from 'shared/ReactWorkTags';
20 import type {ReactNodeList} from 'shared/ReactTypes';
21 import type {ExpirationTime} from './ReactFiberExpirationTime';
22 import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
23 +import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
24
25 import {
26 findCurrentHostFiber,
@@ -294,8 +295,9 @@ export function createContainer(
295 containerInfo: Container,
296 tag: RootTag,
297 hydrate: boolean,
298 + hydrationCallbacks: null | SuspenseHydrationCallbacks,
299 ): OpaqueRoot {
298 - return createFiberRoot(containerInfo, tag, hydrate);
300 + return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
301 }
302
303 export function updateContainer(
packages/react-reconciler/src/ReactFiberRoot.js
+18 -1
@@ -13,11 +13,15 @@ import type {RootTag} from 'shared/ReactRootTags';
13 import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
14 import type {Thenable} from './ReactFiberWorkLoop';
15 import type {Interaction} from 'scheduler/src/Tracing';
16 +import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
17
18 import {noTimeout} from './ReactFiberHostConfig';
19 import {createHostRootFiber} from './ReactFiber';
20 import {NoWork} from './ReactFiberExpirationTime';
20 -import {enableSchedulerTracing} from 'shared/ReactFeatureFlags';
21 +import {
22 + enableSchedulerTracing,
23 + enableSuspenseCallback,
24 +} from 'shared/ReactFeatureFlags';
25 import {unstable_getThreadID} from 'scheduler/tracing';
26
27 // TODO: This should be lifted into the renderer.
@@ -83,6 +87,11 @@ type ProfilingOnlyFiberRootProperties = {|
87 pendingInteractionMap: PendingInteractionMap,
88 |};
89
90 +// The follow fields are only used by enableSuspenseCallback for hydration.
91 +type SuspenseCallbackOnlyFiberRootProperties = {|
92 + hydrationCallbacks: null | SuspenseHydrationCallbacks,
93 +|};
94 +
95 // Exported FiberRoot type includes all properties,
96 // To avoid requiring potentially error-prone :any casts throughout the project.
97 // Profiling properties are only safe to access in profiling builds (when enableSchedulerTracing is true).
@@ -91,6 +100,7 @@ type ProfilingOnlyFiberRootProperties = {|
100 export type FiberRoot = {
101 ...BaseFiberRootProperties,
102 ...ProfilingOnlyFiberRootProperties,
103 + ...SuspenseCallbackOnlyFiberRootProperties,
104 };
105
106 function FiberRootNode(containerInfo, tag, hydrate) {
@@ -117,14 +127,21 @@ function FiberRootNode(containerInfo, tag, hydrate) {
127 this.memoizedInteractions = new Set();
128 this.pendingInteractionMap = new Map();
129 }
130 + if (enableSuspenseCallback) {
131 + this.hydrationCallbacks = null;
132 + }
133 }
134
135 export function createFiberRoot(
136 containerInfo: any,
137 tag: RootTag,
138 hydrate: boolean,
139 + hydrationCallbacks: null | SuspenseHydrationCallbacks,
140 ): FiberRoot {
141 const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
142 + if (enableSuspenseCallback) {
143 + root.hydrationCallbacks = hydrationCallbacks;
144 + }
145
146 // Cyclic construction. This cheats the type system right now because
147 // stateNode is any.
packages/react-reconciler/src/ReactFiberSuspenseComponent.js
+5
@@ -17,6 +17,11 @@ import {
17 isSuspenseInstanceFallback,
18 } from './ReactFiberHostConfig';
19
20 +export type SuspenseHydrationCallbacks = {
21 + onHydrated?: (suspenseInstance: SuspenseInstance) => void,
22 + onDeleted?: (suspenseInstance: SuspenseInstance) => void,
23 +};
24 +
25 // A null SuspenseState represents an unsuspended normal Suspense boundary.
26 // A non-null SuspenseState means that it is blocked for one reason or another.
27 // - A non-null dehydrated field means it's blocked pending hydration.
packages/react-reconciler/src/ReactFiberWorkLoop.js
+4 -3
@@ -1638,6 +1638,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1638 null,
1639 commitMutationEffects,
1640 null,
1641 + root,
1642 renderPriorityLevel,
1643 );
1644 if (hasCaughtError()) {
@@ -1648,7 +1649,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1649 }
1650 } else {
1651 try {
1651 - commitMutationEffects(renderPriorityLevel);
1652 + commitMutationEffects(root, renderPriorityLevel);
1653 } catch (error) {
1654 invariant(nextEffect !== null, 'Should be working on an effect.');
1655 captureCommitPhaseError(nextEffect, error);
@@ -1837,7 +1838,7 @@ function commitBeforeMutationEffects() {
1838 }
1839 }
1840
1840 -function commitMutationEffects(renderPriorityLevel) {
1841 +function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
1842 // TODO: Should probably move the bulk of this function to commitWork.
1843 while (nextEffect !== null) {
1844 setCurrentDebugFiberInDEV(nextEffect);
@@ -1888,7 +1889,7 @@ function commitMutationEffects(renderPriorityLevel) {
1889 break;
1890 }
1891 case Deletion: {
1891 - commitDeletion(nextEffect, renderPriorityLevel);
1892 + commitDeletion(root, nextEffect, renderPriorityLevel);
1893 break;
1894 }
1895 }
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+2
@@ -58,6 +58,7 @@ describe('ReactFiberHostContext', () => {
58 /* root: */ null,
59 ConcurrentRoot,
60 false,
61 + null,
62 );
63 Renderer.updateContainer(
64 <a>
@@ -110,6 +111,7 @@ describe('ReactFiberHostContext', () => {
111 rootContext,
112 ConcurrentRoot,
113 false,
114 + null,
115 );
116 Renderer.updateContainer(
117 <a>
packages/react-test-renderer/src/ReactTestRenderer.js
+1
@@ -442,6 +442,7 @@ const ReactTestRendererFiber = {
442 container,
443 isConcurrent ? ConcurrentRoot : LegacyRoot,
444 false,
445 + null,
446 );
447 invariant(root != null, 'something went wrong');
448 updateContainer(element, root, null, null);
packages/shared/ReactFeatureFlags.js
+2
@@ -84,6 +84,8 @@ export const enableUserBlockingEvents = false;
84 // Add a callback property to suspense to notify which promises are currently
85 // in the update queue. This allows reporting and tracing of what is causing
86 // the user to see a loading state.
87 +// Also allows hydration callbacks to fire when a dehydrated boundary gets
88 +// hydrated or deleted.
89 export const enableSuspenseCallback = false;
90
91 // Part of the simplification of React.createElement so we can eventually move