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