Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c80678c

Browse files
authored
Add "hydrationOptions" behind the enableSuspenseCallback flag (facebook#16434)
This gets invoked when a boundary is either hydrated or if it is deleted because it updated or got deleted before it mounted.
1 parent 2d68bd0 commit c80678c

15 files changed

+292
-28
lines changed

packages/react-art/src/ReactART.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class Surface extends React.Component {
6666

6767
this._surface = Mode.Surface(+width, +height, this._tagRef);
6868

69-
this._mountNode = createContainer(this._surface, LegacyRoot, false);
69+
this._mountNode = createContainer(this._surface, LegacyRoot, false, null);
7070
updateContainer(this.props.children, this._mountNode, this);
7171
}
7272

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ describe('ReactDOMServerPartialHydration', () => {
2424

2525
ReactFeatureFlags = require('shared/ReactFeatureFlags');
2626
ReactFeatureFlags.enableSuspenseServerRenderer = true;
27+
ReactFeatureFlags.enableSuspenseCallback = true;
2728

2829
React = require('react');
2930
ReactDOM = require('react-dom');
@@ -92,6 +93,153 @@ describe('ReactDOMServerPartialHydration', () => {
9293
expect(ref.current).toBe(span);
9394
});
9495

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+
95243
it('warns and replaces the boundary content in legacy mode', async () => {
96244
let suspend = false;
97245
let resolve;

packages/react-dom/src/client/ReactDOM.js

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -367,15 +367,26 @@ ReactWork.prototype._onCommit = function(): void {
367367
function ReactSyncRoot(
368368
container: DOMContainer,
369369
tag: RootTag,
370-
hydrate: boolean,
370+
options: void | RootOptions,
371371
) {
372372
// 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);
374377
this._internalRoot = root;
375378
}
376379

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+
);
379390
this._internalRoot = root;
380391
}
381392

@@ -532,7 +543,15 @@ function legacyCreateRootFromDOMContainer(
532543
}
533544

534545
// 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+
);
536555
}
537556

538557
function legacyRenderSubtreeIntoContainer(
@@ -824,6 +843,10 @@ const ReactDOM: Object = {
824843

825844
type RootOptions = {
826845
hydrate?: boolean,
846+
hydrationOptions?: {
847+
onHydrated?: (suspenseNode: Comment) => void,
848+
onDeleted?: (suspenseNode: Comment) => void,
849+
},
827850
};
828851

829852
function createRoot(
@@ -839,8 +862,7 @@ function createRoot(
839862
functionName,
840863
);
841864
warnIfReactDOMContainerInDEV(container);
842-
const hydrate = options != null && options.hydrate === true;
843-
return new ReactRoot(container, hydrate);
865+
return new ReactRoot(container, options);
844866
}
845867

846868
function createSyncRoot(
@@ -856,8 +878,7 @@ function createSyncRoot(
856878
functionName,
857879
);
858880
warnIfReactDOMContainerInDEV(container);
859-
const hydrate = options != null && options.hydrate === true;
860-
return new ReactSyncRoot(container, BatchedRoot, hydrate);
881+
return new ReactSyncRoot(container, BatchedRoot, options);
861882
}
862883

863884
function warnIfReactDOMContainerInDEV(container) {

packages/react-native-renderer/src/ReactFabric.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ const ReactFabric: ReactFabricType = {
144144
if (!root) {
145145
// TODO (bvaughn): If we decide to keep the wrapper component,
146146
// 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);
148148
roots.set(containerTag, root);
149149
}
150150
updateContainer(element, root, null, callback);

packages/react-native-renderer/src/ReactNativeRenderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ const ReactNativeRenderer: ReactNativeType = {
141141
if (!root) {
142142
// TODO (bvaughn): If we decide to keep the wrapper component,
143143
// 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);
145145
roots.set(containerTag, root);
146146
}
147147
updateContainer(element, root, null, callback);

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
908908
if (!root) {
909909
const container = {rootID: rootID, pendingChildren: [], children: []};
910910
rootContainers.set(rootID, container);
911-
root = NoopRenderer.createContainer(container, tag, false);
911+
root = NoopRenderer.createContainer(container, tag, false, null);
912912
roots.set(rootID, root);
913913
}
914914
return root.current.stateNode.containerInfo;
@@ -925,6 +925,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
925925
container,
926926
ConcurrentRoot,
927927
false,
928+
null,
928929
);
929930
return {
930931
_Scheduler: Scheduler,
@@ -950,6 +951,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
950951
container,
951952
BatchedRoot,
952953
false,
954+
null,
953955
);
954956
return {
955957
_Scheduler: Scheduler,

0 commit comments

Comments
 (0)