@samitouri / QOS-React / commits / 7687962249

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