ReactDOM.createRoot (#11225)
* ReactDOM.createRoot Introduce new API for creating roots. Only root.render and root.unmount are implemented. Later we'll add root.prerender, and support for lazy roots (roots with DOM containers that resolve lazily). * Add hydrate option to createRoot
Andrew Clark committed
Oct 13, 2017 at 20:08 UTC
7687962249789f287040f3b720a5fe03c36e5d5c
13 files changed
+149
-33
src/renderers/dom/fiber/ReactDOMFiberEntry.js
+33
-1
@@ -732,7 +732,7 @@ function renderSubtreeIntoContainer(
732
);
733
}
734
}
735
- const newRoot = DOMRenderer.createContainer(container);
735
+ const newRoot = DOMRenderer.createContainer(container, shouldHydrate);
736
root = container._reactRootContainer = newRoot;
737
// Initial mount should not be batched.
738
DOMRenderer.unbatchedUpdates(() => {
@@ -757,7 +757,39 @@ function createPortal(
757
return ReactPortal.createPortal(children, container, null, key);
758
}
759
760
+type ReactRootNode = {
761
+ render(children: ReactNodeList, callback: ?() => mixed): void,
762
+ unmount(callback: ?() => mixed): void,
763
+
764
+ _reactRootContainer: *,
765
+};
766
+
767
+type RootOptions = {
768
+ hydrate?: boolean,
769
+};
770
+
771
+function ReactRoot(container: Container, hydrate: boolean) {
772
+ const root = DOMRenderer.createContainer(container, hydrate);
773
+ this._reactRootContainer = root;
774
+}
775
+ReactRoot.prototype.render = function(
776
+ children: ReactNodeList,
777
+ callback: ?() => mixed,
778
+): void {
779
+ const root = this._reactRootContainer;
780
+ DOMRenderer.updateContainer(children, root, null, callback);
781
+};
782
+ReactRoot.prototype.unmount = function(callback) {
783
+ const root = this._reactRootContainer;
784
+ DOMRenderer.updateContainer(null, root, null, callback);
785
+};
786
+
787
var ReactDOMFiber = {
788
+ createRoot(container: DOMContainer, options?: RootOptions): ReactRootNode {
789
+ const hydrate = options != null && options.hydrate === true;
790
+ return new ReactRoot(container, hydrate);
791
+ },
792
+
793
createPortal,
794
795
findDOMNode(
src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js
new
+71
@@ -0,0 +1,71 @@
1
+/**
2
+ * Copyright (c) 2013-present, Facebook, Inc.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+var React = require('react');
13
+var ReactDOM = require('react-dom');
14
+var ReactDOMServer = require('react-dom/server');
15
+
16
+describe('ReactDOMRoot', () => {
17
+ let container;
18
+
19
+ beforeEach(() => {
20
+ container = document.createElement('div');
21
+ });
22
+
23
+ it('renders children', () => {
24
+ const root = ReactDOM.createRoot(container);
25
+ root.render(<div>Hi</div>);
26
+ expect(container.textContent).toEqual('Hi');
27
+ });
28
+
29
+ it('unmounts children', () => {
30
+ const root = ReactDOM.createRoot(container);
31
+ root.render(<div>Hi</div>);
32
+ expect(container.textContent).toEqual('Hi');
33
+ root.unmount();
34
+ expect(container.textContent).toEqual('');
35
+ });
36
+
37
+ it('supports hydration', async () => {
38
+ const markup = await new Promise(resolve =>
39
+ resolve(
40
+ ReactDOMServer.renderToString(<div><span className="extra" /></div>),
41
+ ),
42
+ );
43
+
44
+ spyOn(console, 'error');
45
+
46
+ // Does not hydrate by default
47
+ const container1 = document.createElement('div');
48
+ container1.innerHTML = markup;
49
+ const root1 = ReactDOM.createRoot(container1);
50
+ root1.render(<div><span /></div>);
51
+ expect(console.error.calls.count()).toBe(0);
52
+
53
+ // Accepts `hydrate` option
54
+ const container2 = document.createElement('div');
55
+ container2.innerHTML = markup;
56
+ const root2 = ReactDOM.createRoot(container2, {hydrate: true});
57
+ root2.render(<div><span /></div>);
58
+ expect(console.error.calls.count()).toBe(1);
59
+ expect(console.error.calls.argsFor(0)[0]).toMatch('Extra attributes');
60
+ });
61
+
62
+ it('does not clear existing children', async () => {
63
+ spyOn(console, 'error');
64
+ container.innerHTML = '<div>a</div><div>b</div>';
65
+ const root = ReactDOM.createRoot(container);
66
+ root.render(<div><span>c</span><span>d</span></div>);
67
+ expect(container.textContent).toEqual('abcd');
68
+ root.render(<div><span>d</span><span>c</span></div>);
69
+ expect(container.textContent).toEqual('abdc');
70
+ });
71
+});
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+6
@@ -2638,6 +2638,12 @@ describe('ReactDOMServerIntegration', () => {
2638
it('should error reconnecting different element types', () =>
2639
expectMarkupMismatch(<div />, <span />));
2640
2641
+ it('should error reconnecting fewer root children', () =>
2642
+ expectMarkupMismatch(<span key="a" />, [
2643
+ <span key="a" />,
2644
+ <span key="b" />,
2645
+ ]));
2646
+
2647
it('should error reconnecting missing attributes', () =>
2648
expectMarkupMismatch(<div id="foo" />, <div />));
2649
src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js
+1
-1
@@ -396,7 +396,7 @@ describe('rendering React components at document', () => {
396
expect(container.textContent).toBe('parsnip');
397
expectDev(console.error.calls.count()).toBe(1);
398
expectDev(console.error.calls.argsFor(0)[0]).toContain(
399
- 'Did not expect server HTML to contain the text node "potato" in <div>.',
399
+ 'Expected server HTML to contain a matching <div> in <div>.',
400
);
401
});
402
src/renderers/native-cs/ReactNativeCSFiberEntry.js
+1
-1
@@ -194,7 +194,7 @@ const ReactNativeCSFiber: ReactNativeCSType = {
194
if (!root) {
195
// TODO (bvaughn): If we decide to keep the wrapper component,
196
// We could create a wrapper for containerTag as well to reduce special casing.
197
- root = ReactNativeCSFiberRenderer.createContainer(containerTag);
197
+ root = ReactNativeCSFiberRenderer.createContainer(containerTag, false);
198
roots.set(containerTag, root);
199
}
200
ReactNativeCSFiberRenderer.updateContainer(element, root, null, callback);
src/renderers/native-rt/ReactNativeRTFiberEntry.js
+1
-1
@@ -53,7 +53,7 @@ const ReactNativeRTFiber: ReactNativeRTType = {
53
if (!root) {
54
// TODO (bvaughn): If we decide to keep the wrapper component,
55
// We could create a wrapper for containerTag as well to reduce special casing.
56
- root = ReactNativeRTFiberRenderer.createContainer(containerTag);
56
+ root = ReactNativeRTFiberRenderer.createContainer(containerTag, false);
57
roots.set(containerTag, root);
58
}
59
ReactNativeRTFiberRenderer.updateContainer(element, root, null, callback);
src/renderers/native/ReactNativeFiberEntry.js
+1
-1
@@ -52,7 +52,7 @@ const ReactNativeFiber: ReactNativeType = {
52
if (!root) {
53
// TODO (bvaughn): If we decide to keep the wrapper component,
54
// We could create a wrapper for containerTag as well to reduce special casing.
55
- root = ReactNativeFiberRenderer.createContainer(containerTag);
55
+ root = ReactNativeFiberRenderer.createContainer(containerTag, false);
56
roots.set(containerTag, root);
57
}
58
ReactNativeFiberRenderer.updateContainer(element, root, null, callback);
src/renderers/noop/ReactNoopEntry.js
+1
-1
@@ -269,7 +269,7 @@ var ReactNoop = {
269
if (!root) {
270
const container = {rootID: rootID, children: []};
271
rootContainers.set(rootID, container);
272
- root = NoopRenderer.createContainer(container);
272
+ root = NoopRenderer.createContainer(container, false);
273
roots.set(rootID, root);
274
}
275
NoopRenderer.updateContainer(element, root, null, callback);
src/renderers/shared/fiber/ReactFiberBeginWork.js
+2
@@ -338,8 +338,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
338
return bailoutOnAlreadyFinishedWork(current, workInProgress);
339
}
340
const element = state.element;
341
+ const root: FiberRoot = workInProgress.stateNode;
342
if (
343
(current === null || current.child === null) &&
344
+ root.hydrate &&
345
enterHydrationState(workInProgress)
346
) {
347
// If we don't have any current children this might be the first pass.
src/renderers/shared/fiber/ReactFiberHydrationContext.js
+21
-22
@@ -77,9 +77,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
77
didNotMatchHydratedTextInstance,
78
didNotHydrateContainerInstance,
79
didNotHydrateInstance,
80
- // TODO: These are currently unused, see below.
81
- // didNotFindHydratableContainerInstance,
82
- // didNotFindHydratableContainerTextInstance,
80
+ didNotFindHydratableContainerInstance,
81
+ didNotFindHydratableContainerTextInstance,
82
didNotFindHydratableInstance,
83
didNotFindHydratableTextInstance,
84
} = hydration;
@@ -140,25 +139,25 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
139
fiber.effectTag |= Placement;
140
if (__DEV__) {
141
switch (returnFiber.tag) {
143
- // TODO: Currently we don't warn for insertions into the root because
144
- // we always insert into the root in the non-hydrating case. We just
145
- // delete the existing content. Reenable this once we have a better
146
- // strategy for determining if we're hydrating or not.
147
- // case HostRoot: {
148
- // const parentContainer = returnFiber.stateNode.containerInfo;
149
- // switch (fiber.tag) {
150
- // case HostComponent:
151
- // const type = fiber.type;
152
- // const props = fiber.pendingProps;
153
- // didNotFindHydratableContainerInstance(parentContainer, type, props);
154
- // break;
155
- // case HostText:
156
- // const text = fiber.pendingProps;
157
- // didNotFindHydratableContainerTextInstance(parentContainer, text);
158
- // break;
159
- // }
160
- // break;
161
- // }
142
+ case HostRoot: {
143
+ const parentContainer = returnFiber.stateNode.containerInfo;
144
+ switch (fiber.tag) {
145
+ case HostComponent:
146
+ const type = fiber.type;
147
+ const props = fiber.pendingProps;
148
+ didNotFindHydratableContainerInstance(
149
+ parentContainer,
150
+ type,
151
+ props,
152
+ );
153
+ break;
154
+ case HostText:
155
+ const text = fiber.pendingProps;
156
+ didNotFindHydratableContainerTextInstance(parentContainer, text);
157
+ break;
158
+ }
159
+ break;
160
+ }
161
case HostComponent: {
162
const parentType = returnFiber.type;
163
const parentProps = returnFiber.memoizedProps;
src/renderers/shared/fiber/ReactFiberReconciler.js
+3
-3
@@ -219,7 +219,7 @@ type HydrationHostConfig<T, P, I, TI, C, CX, PL> = {
219
};
220
221
export type Reconciler<C, I, TI> = {
222
- createContainer(containerInfo: C): OpaqueRoot,
222
+ createContainer(containerInfo: C, hydrate: boolean): OpaqueRoot,
223
updateContainer(
224
element: ReactNodeList,
225
container: OpaqueRoot,
@@ -335,8 +335,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
335
}
336
337
return {
338
- createContainer(containerInfo: C): OpaqueRoot {
339
- return createFiberRoot(containerInfo);
338
+ createContainer(containerInfo: C, hydrate: boolean): OpaqueRoot {
339
+ return createFiberRoot(containerInfo, hydrate);
340
},
341
342
updateContainer(
src/renderers/shared/fiber/ReactFiberRoot.js
+7
-1
@@ -26,9 +26,14 @@ export type FiberRoot = {
26
// Top context object, used by renderSubtreeIntoContainer
27
context: Object | null,
28
pendingContext: Object | null,
29
+ // Determines if we should attempt to hydrate on the initial mount
30
+ +hydrate: boolean,
31
};
32
31
-exports.createFiberRoot = function(containerInfo: any): FiberRoot {
33
+exports.createFiberRoot = function(
34
+ containerInfo: any,
35
+ hydrate: boolean,
36
+): FiberRoot {
37
// Cyclic construction. This cheats the type system right now because
38
// stateNode is any.
39
const uninitializedFiber = createHostRootFiber();
@@ -39,6 +44,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot {
44
nextScheduledRoot: null,
45
context: null,
46
pendingContext: null,
47
+ hydrate,
48
};
49
uninitializedFiber.stateNode = root;
50
return root;
src/renderers/testing/ReactTestRendererFiberEntry.js
+1
-1
@@ -574,7 +574,7 @@ var ReactTestRendererFiber = {
574
createNodeMock,
575
tag: 'CONTAINER',
576
};
577
- var root: FiberRoot | null = TestRenderer.createContainer(container);
577
+ var root: FiberRoot | null = TestRenderer.createContainer(container, false);
578
invariant(root != null, 'something went wrong');
579
TestRenderer.updateContainer(element, root, null, null);
580