@samitouri / QOS-React / commits / 7ec4c55971

createRoot(..., {hydrate:true}) -> hydrateRoot(...) (#21687)

This adds a new top level API for hydrating a root. It takes the initial children as part of its constructor. These are unlike other render calls in that they have to represent what the server sent and they can't be batched with other updates. I also changed the options to move the hydrationOptions to the top level since now these options are all hydration options. I kept the createRoot one just temporarily to make it easier to codemod internally but I'm doing a follow up to delete. As part of this I un-dried a couple of paths. ReactDOMLegacy was intended to be built on top of the new API but it didn't actually use those root APIs because there are special paths. It also doesn't actually use most of the commmon paths since all the options are ignored. It also made it hard to add only warnings for legacy only or new only code paths. I also forked the create/hydrate paths because they're subtly different since now the options are different. The containers are also different because I now error for comment nodes during hydration which just doesn't work at all but eventually we'll error for all createRoot calls. After some iteration it might make sense to break out some common paths but for now it's easier to iterate on the duplicates.

Sebastian Markbåge committed Jun 15, 2021 at 16:37 UTC 7ec4c55971aad644616ca0b040f42410659fe802
11 files changed +130 -59
packages/react-dom/index.classic.fb.js
+1
@@ -23,6 +23,7 @@ export {
23 createPortal,
24 createRoot,
25 createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot
26 + hydrateRoot,
27 findDOMNode,
28 flushSync,
29 hydrate,
packages/react-dom/index.experimental.js
+1
@@ -11,6 +11,7 @@ export {
11 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
12 createPortal,
13 createRoot,
14 + hydrateRoot,
15 findDOMNode,
16 flushSync,
17 hydrate,
packages/react-dom/index.js
+1
@@ -14,6 +14,7 @@ export {
14 createPortal,
15 createRoot,
16 createRoot as unstable_createRoot,
17 + hydrateRoot,
18 findDOMNode,
19 flushSync,
20 hydrate,
packages/react-dom/index.modern.fb.js
+1
@@ -12,6 +12,7 @@ export {
12 createPortal,
13 createRoot,
14 createRoot as unstable_createRoot, // TODO Remove once callsites use createRoot
15 + hydrateRoot,
16 flushSync,
17 unstable_batchedUpdates,
18 unstable_createEventHandle,
packages/react-dom/index.stable.js
+1
@@ -11,6 +11,7 @@ export {
11 __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
12 createPortal,
13 createRoot,
14 + hydrateRoot,
15 findDOMNode,
16 flushSync,
17 hydrate,
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+3 -4
@@ -96,11 +96,10 @@ describe('ReactDOMRoot', () => {
96 );
97 Scheduler.unstable_flushAll();
98
99 - // Accepts `hydrate` option
99 const container2 = document.createElement('div');
100 container2.innerHTML = markup;
102 - const root2 = ReactDOM.createRoot(container2, {hydrate: true});
103 - root2.render(
101 + ReactDOM.hydrateRoot(
102 + container2,
103 <div>
104 <span />
105 </div>,
@@ -191,7 +190,7 @@ describe('ReactDOMRoot', () => {
190 // We care about this warning:
191 'You are calling ReactDOM.hydrate() on a container that was previously ' +
192 'passed to ReactDOM.createRoot(). This is not supported. ' +
194 - 'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
193 + 'Did you mean to call hydrateRoot(container, element)?',
194 // This is more of a symptom but restructuring the code to avoid it isn't worth it:
195 'Replacing React-rendered children with a new root component.',
196 ],
packages/react-dom/src/client/ReactDOM.js
+2 -1
@@ -17,7 +17,7 @@ import {
17 unstable_renderSubtreeIntoContainer,
18 unmountComponentAtNode,
19 } from './ReactDOMLegacy';
20 -import {createRoot, isValidContainer} from './ReactDOMRoot';
20 +import {createRoot, hydrateRoot, isValidContainer} from './ReactDOMRoot';
21 import {createEventHandle} from './ReactDOMEventHandle';
22
23 import {
@@ -182,6 +182,7 @@ export {
182 unmountComponentAtNode,
183 // exposeConcurrentModeAPIs
184 createRoot,
185 + hydrateRoot,
186 flushControlled as unstable_flushControlled,
187 scheduleHydration as unstable_scheduleHydration,
188 // Disabled behind disableUnstableRenderSubtreeIntoContainer
packages/react-dom/src/client/ReactDOMHostConfig.js
+2 -3
@@ -14,7 +14,6 @@ import type {
14 IntersectionObserverOptions,
15 ObserveVisibleRectsCallback,
16 } from 'react-reconciler/src/ReactTestSelectors';
17 -import type {RootType} from './ReactDOMRoot';
17 import type {ReactScopeInstance} from 'shared/ReactTypes';
18
19 import {
@@ -105,8 +104,8 @@ export type EventTargetChildElement = {
104 ...
105 };
106 export type Container =
108 - | (Element & {_reactRootContainer?: RootType, ...})
109 - | (Document & {_reactRootContainer?: RootType, ...});
107 + | (Element & {_reactRootContainer?: FiberRoot, ...})
108 + | (Document & {_reactRootContainer?: FiberRoot, ...});
109 export type Instance = Element;
110 export type TextInstance = Text;
111 export type SuspenseInstance = Comment & {_reactRetry?: () => void, ...};
packages/react-dom/src/client/ReactDOMLegacy.js
+29 -19
@@ -8,16 +8,17 @@
8 */
9
10 import type {Container} from './ReactDOMHostConfig';
11 -import type {RootType} from './ReactDOMRoot';
11 import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
12 import type {ReactNodeList} from 'shared/ReactTypes';
13
14 import {
15 getInstanceFromNode,
16 isContainerMarkedAsRoot,
17 + markContainerAsRoot,
18 unmarkContainerAsRoot,
19 } from './ReactDOMComponentTree';
20 -import {createLegacyRoot, isValidContainer} from './ReactDOMRoot';
20 +import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
21 +import {isValidContainerLegacy} from './ReactDOMRoot';
22 import {
23 DOCUMENT_NODE,
24 ELEMENT_NODE,
@@ -25,6 +26,7 @@ import {
26 } from '../shared/HTMLNodeType';
27
28 import {
29 + createContainer,
30 findHostInstanceWithNoPortals,
31 updateContainer,
32 unbatchedUpdates,
@@ -32,6 +34,7 @@ import {
34 findHostInstance,
35 findHostInstanceWithWarning,
36 } from 'react-reconciler/src/ReactFiberReconciler';
37 +import {LegacyRoot} from 'react-reconciler/src/ReactRootTags';
38 import getComponentNameFromType from 'shared/getComponentNameFromType';
39 import invariant from 'shared/invariant';
40 import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -45,7 +48,7 @@ if (__DEV__) {
48 topLevelUpdateWarnings = (container: Container) => {
49 if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) {
50 const hostInstance = findHostInstanceWithNoPortals(
48 - container._reactRootContainer._internalRoot.current,
51 + container._reactRootContainer.current,
52 );
53 if (hostInstance) {
54 if (hostInstance.parentNode !== container) {
@@ -103,7 +106,7 @@ function getReactRootElementInContainer(container: any) {
106 function legacyCreateRootFromDOMContainer(
107 container: Container,
108 forceHydrate: boolean,
106 -): RootType {
109 +): FiberRoot {
110 // First clear any existing content.
111 if (!forceHydrate) {
112 let rootSibling;
@@ -112,14 +115,21 @@ function legacyCreateRootFromDOMContainer(
115 }
116 }
117
115 - return createLegacyRoot(
118 + const root = createContainer(
119 container,
117 - forceHydrate
118 - ? {
119 - hydrate: true,
120 - }
121 - : undefined,
120 + LegacyRoot,
121 + forceHydrate,
122 + null, // hydrationCallbacks
123 + false, // isStrictMode
124 + false, // concurrentUpdatesByDefaultOverride,
125 );
126 + markContainerAsRoot(root.current, container);
127 +
128 + const rootContainerElement =
129 + container.nodeType === COMMENT_NODE ? container.parentNode : container;
130 + listenToAllSupportedEvents(rootContainerElement);
131 +
132 + return root;
133 }
134
135 function warnOnInvalidCallback(callback: mixed, callerName: string): void {
@@ -155,7 +165,7 @@ function legacyRenderSubtreeIntoContainer(
165 container,
166 forceHydrate,
167 );
158 - fiberRoot = root._internalRoot;
168 + fiberRoot = root;
169 if (typeof callback === 'function') {
170 const originalCallback = callback;
171 callback = function() {
@@ -168,7 +178,7 @@ function legacyRenderSubtreeIntoContainer(
178 updateContainer(children, fiberRoot, parentComponent, callback);
179 });
180 } else {
171 - fiberRoot = root._internalRoot;
181 + fiberRoot = root;
182 if (typeof callback === 'function') {
183 const originalCallback = callback;
184 callback = function() {
@@ -221,7 +231,7 @@ export function hydrate(
231 ) {
232 if (__DEV__) {
233 console.error(
224 - 'ReactDOM.hydrate is no longer supported in React 18. Use createRoot ' +
234 + 'ReactDOM.hydrate is no longer supported in React 18. Use hydrateRoot ' +
235 'instead. Until you switch to the new API, your app will behave as ' +
236 "if it's running React 17. Learn " +
237 'more: https://reactjs.org/link/switch-to-createroot',
@@ -229,7 +239,7 @@ export function hydrate(
239 }
240
241 invariant(
232 - isValidContainer(container),
242 + isValidContainerLegacy(container),
243 'Target container is not a DOM element.',
244 );
245 if (__DEV__) {
@@ -240,7 +250,7 @@ export function hydrate(
250 console.error(
251 'You are calling ReactDOM.hydrate() on a container that was previously ' +
252 'passed to ReactDOM.createRoot(). This is not supported. ' +
243 - 'Did you mean to call createRoot(container, {hydrate: true}).render(element)?',
253 + 'Did you mean to call hydrateRoot(container, element)?',
254 );
255 }
256 }
@@ -269,7 +279,7 @@ export function render(
279 }
280
281 invariant(
272 - isValidContainer(container),
282 + isValidContainerLegacy(container),
283 'Target container is not a DOM element.',
284 );
285 if (__DEV__) {
@@ -300,7 +310,7 @@ export function unstable_renderSubtreeIntoContainer(
310 callback: ?Function,
311 ) {
312 invariant(
303 - isValidContainer(containerNode),
313 + isValidContainerLegacy(containerNode),
314 'Target container is not a DOM element.',
315 );
316 invariant(
@@ -318,7 +328,7 @@ export function unstable_renderSubtreeIntoContainer(
328
329 export function unmountComponentAtNode(container: Container) {
330 invariant(
321 - isValidContainer(container),
331 + isValidContainerLegacy(container),
332 'unmountComponentAtNode(...): Target container is not a DOM element.',
333 );
334
@@ -365,7 +375,7 @@ export function unmountComponentAtNode(container: Container) {
375 // Check if the container itself is a React root node.
376 const isContainerReactRoot =
377 container.nodeType === ELEMENT_NODE &&
368 - isValidContainer(container.parentNode) &&
378 + isValidContainerLegacy(container.parentNode) &&
379 !!container.parentNode._reactRootContainer;
380
381 if (hasNonRootReactChild) {
packages/react-dom/src/client/ReactDOMRoot.js
+87 -31
@@ -8,7 +8,6 @@
8 */
9
10 import type {Container} from './ReactDOMHostConfig';
11 -import type {RootTag} from 'react-reconciler/src/ReactRootTags';
11 import type {MutableSource, ReactNodeList} from 'shared/ReactTypes';
12 import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
13
@@ -19,7 +18,8 @@ export type RootType = {
18 ...
19 };
20
22 -export type RootOptions = {
21 +export type CreateRootOptions = {
22 + // TODO: Remove these options.
23 hydrate?: boolean,
24 hydrationOptions?: {
25 onHydrated?: (suspenseNode: Comment) => void,
@@ -27,6 +27,18 @@ export type RootOptions = {
27 mutableSources?: Array<MutableSource<any>>,
28 ...
29 },
30 + // END OF TODO
31 + unstable_strictMode?: boolean,
32 + unstable_concurrentUpdatesByDefault?: boolean,
33 + ...
34 +};
35 +
36 +export type HydrateRootOptions = {
37 + // Hydration options
38 + hydratedSources?: Array<MutableSource<any>>,
39 + onHydrated?: (suspenseNode: Comment) => void,
40 + onDeleted?: (suspenseNode: Comment) => void,
41 + // Options for all roots
42 unstable_strictMode?: boolean,
43 unstable_concurrentUpdatesByDefault?: boolean,
44 ...
@@ -52,20 +64,14 @@ import {
64 registerMutableSourceForHydration,
65 } from 'react-reconciler/src/ReactFiberReconciler';
66 import invariant from 'shared/invariant';
55 -import {ConcurrentRoot, LegacyRoot} from 'react-reconciler/src/ReactRootTags';
67 +import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
68 import {allowConcurrentByDefault} from 'shared/ReactFeatureFlags';
69
58 -function ReactDOMRoot(container: Container, options: void | RootOptions) {
59 - this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
60 -}
61 -
62 -function ReactDOMLegacyRoot(container: Container, options: void | RootOptions) {
63 - this._internalRoot = createRootImpl(container, LegacyRoot, options);
70 +function ReactDOMRoot(internalRoot) {
71 + this._internalRoot = internalRoot;
72 }
73
66 -ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
67 - children: ReactNodeList,
68 -): void {
74 +ReactDOMRoot.prototype.render = function(children: ReactNodeList): void {
75 const root = this._internalRoot;
76 if (__DEV__) {
77 if (typeof arguments[1] === 'function') {
@@ -93,7 +99,7 @@ ReactDOMRoot.prototype.render = ReactDOMLegacyRoot.prototype.render = function(
99 updateContainer(children, root, null, null);
100 };
101
96 -ReactDOMRoot.prototype.unmount = ReactDOMLegacyRoot.prototype.unmount = function(): void {
102 +ReactDOMRoot.prototype.unmount = function(): void {
103 if (__DEV__) {
104 if (typeof arguments[0] === 'function') {
105 console.error(
@@ -109,12 +115,17 @@ ReactDOMRoot.prototype.unmount = ReactDOMLegacyRoot.prototype.unmount = function
115 });
116 };
117
112 -function createRootImpl(
118 +export function createRoot(
119 container: Container,
114 - tag: RootTag,
115 - options: void | RootOptions,
116 -) {
117 - // Tag is either LegacyRoot or Concurrent Root
120 + options?: CreateRootOptions,
121 +): RootType {
122 + invariant(
123 + isValidContainerLegacy(container),
124 + 'createRoot(...): Target container is not a DOM element.',
125 + );
126 + warnIfReactDOMContainerInDEV(container);
127 +
128 + // TODO: Delete these options
129 const hydrate = options != null && options.hydrate === true;
130 const hydrationCallbacks =
131 (options != null && options.hydrationOptions) || null;
@@ -123,8 +134,9 @@ function createRootImpl(
134 options.hydrationOptions != null &&
135 options.hydrationOptions.mutableSources) ||
136 null;
126 - const isStrictMode = options != null && options.unstable_strictMode === true;
137 + // END TODO
138
139 + const isStrictMode = options != null && options.unstable_strictMode === true;
140 let concurrentUpdatesByDefaultOverride = null;
141 if (allowConcurrentByDefault) {
142 concurrentUpdatesByDefaultOverride =
@@ -135,7 +147,7 @@ function createRootImpl(
147
148 const root = createContainer(
149 container,
138 - tag,
150 + ConcurrentRoot,
151 hydrate,
152 hydrationCallbacks,
153 isStrictMode,
@@ -147,36 +159,80 @@ function createRootImpl(
159 container.nodeType === COMMENT_NODE ? container.parentNode : container;
160 listenToAllSupportedEvents(rootContainerElement);
161
162 + // TODO: Delete this path
163 if (mutableSources) {
164 for (let i = 0; i < mutableSources.length; i++) {
165 const mutableSource = mutableSources[i];
166 registerMutableSourceForHydration(root, mutableSource);
167 }
168 }
169 + // END TODO
170
157 - return root;
171 + return new ReactDOMRoot(root);
172 }
173
160 -export function createRoot(
174 +export function hydrateRoot(
175 container: Container,
162 - options?: RootOptions,
176 + initialChildren: ReactNodeList,
177 + options?: HydrateRootOptions,
178 ): RootType {
179 invariant(
180 isValidContainer(container),
166 - 'createRoot(...): Target container is not a DOM element.',
181 + 'hydrateRoot(...): Target container is not a DOM element.',
182 );
183 warnIfReactDOMContainerInDEV(container);
169 - return new ReactDOMRoot(container, options);
184 +
185 + // For now we reuse the whole bag of options since they contain
186 + // the hydration callbacks.
187 + const hydrationCallbacks = options != null ? options : null;
188 + const mutableSources = (options != null && options.hydratedSources) || null;
189 + const isStrictMode = options != null && options.unstable_strictMode === true;
190 +
191 + let concurrentUpdatesByDefaultOverride = null;
192 + if (allowConcurrentByDefault) {
193 + concurrentUpdatesByDefaultOverride =
194 + options != null && options.unstable_concurrentUpdatesByDefault != null
195 + ? options.unstable_concurrentUpdatesByDefault
196 + : null;
197 + }
198 +
199 + const root = createContainer(
200 + container,
201 + ConcurrentRoot,
202 + true, // hydrate
203 + hydrationCallbacks,
204 + isStrictMode,
205 + concurrentUpdatesByDefaultOverride,
206 + );
207 + markContainerAsRoot(root.current, container);
208 + // This can't be a comment node since hydration doesn't work on comment nodes anyway.
209 + listenToAllSupportedEvents(container);
210 +
211 + if (mutableSources) {
212 + for (let i = 0; i < mutableSources.length; i++) {
213 + const mutableSource = mutableSources[i];
214 + registerMutableSourceForHydration(root, mutableSource);
215 + }
216 + }
217 +
218 + // Render the initial children
219 + updateContainer(initialChildren, root, null, null);
220 +
221 + return new ReactDOMRoot(root);
222 }
223
172 -export function createLegacyRoot(
173 - container: Container,
174 - options?: RootOptions,
175 -): RootType {
176 - return new ReactDOMLegacyRoot(container, options);
224 +export function isValidContainer(node: any): boolean {
225 + return !!(
226 + node &&
227 + (node.nodeType === ELEMENT_NODE ||
228 + node.nodeType === DOCUMENT_NODE ||
229 + node.nodeType === DOCUMENT_FRAGMENT_NODE)
230 + );
231 }
232
179 -export function isValidContainer(node: mixed): boolean {
233 +// TODO: Remove this function which also includes comment nodes.
234 +// We only use it in places that are currently more relaxed.
235 +export function isValidContainerLegacy(node: any): boolean {
236 return !!(
237 node &&
238 (node.nodeType === ELEMENT_NODE ||
scripts/error-codes/codes.json
+2 -1
@@ -392,5 +392,6 @@
392 "401": "The stacks must reach the root at the same time. This is a bug in React.",
393 "402": "The depth must equal at least at zero before reaching the root. This is a bug in React.",
394 "403": "Tried to pop a Context at the root of the app. This is a bug in React.",
395 - "404": "Invalid hook call. Hooks can only be called inside of the body of a function component."
395 + "404": "Invalid hook call. Hooks can only be called inside of the body of a function component.",
396 + "405": "hydrateRoot(...): Target container is not a DOM element."
397 }