Skip to content

Commit

Permalink
ReactDOM.createRoot (#11225)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
acdlite authored Oct 14, 2017
1 parent 3ffb5d0 commit 7687962
Show file tree
Hide file tree
Showing 13 changed files with 149 additions and 33 deletions.
34 changes: 33 additions & 1 deletion src/renderers/dom/fiber/ReactDOMFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ function renderSubtreeIntoContainer(
);
}
}
const newRoot = DOMRenderer.createContainer(container);
const newRoot = DOMRenderer.createContainer(container, shouldHydrate);
root = container._reactRootContainer = newRoot;
// Initial mount should not be batched.
DOMRenderer.unbatchedUpdates(() => {
Expand All @@ -757,7 +757,39 @@ function createPortal(
return ReactPortal.createPortal(children, container, null, key);
}

type ReactRootNode = {
render(children: ReactNodeList, callback: ?() => mixed): void,
unmount(callback: ?() => mixed): void,

_reactRootContainer: *,
};

type RootOptions = {
hydrate?: boolean,
};

function ReactRoot(container: Container, hydrate: boolean) {
const root = DOMRenderer.createContainer(container, hydrate);
this._reactRootContainer = root;
}
ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): void {
const root = this._reactRootContainer;
DOMRenderer.updateContainer(children, root, null, callback);
};
ReactRoot.prototype.unmount = function(callback) {
const root = this._reactRootContainer;
DOMRenderer.updateContainer(null, root, null, callback);
};

var ReactDOMFiber = {
createRoot(container: DOMContainer, options?: RootOptions): ReactRootNode {
const hydrate = options != null && options.hydrate === true;
return new ReactRoot(container, hydrate);
},

createPortal,

findDOMNode(
Expand Down
71 changes: 71 additions & 0 deletions src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/

'use strict';

var React = require('react');
var ReactDOM = require('react-dom');
var ReactDOMServer = require('react-dom/server');

describe('ReactDOMRoot', () => {
let container;

beforeEach(() => {
container = document.createElement('div');
});

it('renders children', () => {
const root = ReactDOM.createRoot(container);
root.render(<div>Hi</div>);
expect(container.textContent).toEqual('Hi');
});

it('unmounts children', () => {
const root = ReactDOM.createRoot(container);
root.render(<div>Hi</div>);
expect(container.textContent).toEqual('Hi');
root.unmount();
expect(container.textContent).toEqual('');
});

it('supports hydration', async () => {
const markup = await new Promise(resolve =>
resolve(
ReactDOMServer.renderToString(<div><span className="extra" /></div>),
),
);

spyOn(console, 'error');

// Does not hydrate by default
const container1 = document.createElement('div');
container1.innerHTML = markup;
const root1 = ReactDOM.createRoot(container1);
root1.render(<div><span /></div>);
expect(console.error.calls.count()).toBe(0);

// Accepts `hydrate` option
const container2 = document.createElement('div');
container2.innerHTML = markup;
const root2 = ReactDOM.createRoot(container2, {hydrate: true});
root2.render(<div><span /></div>);
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toMatch('Extra attributes');
});

it('does not clear existing children', async () => {
spyOn(console, 'error');
container.innerHTML = '<div>a</div><div>b</div>';
const root = ReactDOM.createRoot(container);
root.render(<div><span>c</span><span>d</span></div>);
expect(container.textContent).toEqual('abcd');
root.render(<div><span>d</span><span>c</span></div>);
expect(container.textContent).toEqual('abdc');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2638,6 +2638,12 @@ describe('ReactDOMServerIntegration', () => {
it('should error reconnecting different element types', () =>
expectMarkupMismatch(<div />, <span />));

it('should error reconnecting fewer root children', () =>
expectMarkupMismatch(<span key="a" />, [
<span key="a" />,
<span key="b" />,
]));

it('should error reconnecting missing attributes', () =>
expectMarkupMismatch(<div id="foo" />, <div />));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ describe('rendering React components at document', () => {
expect(container.textContent).toBe('parsnip');
expectDev(console.error.calls.count()).toBe(1);
expectDev(console.error.calls.argsFor(0)[0]).toContain(
'Did not expect server HTML to contain the text node "potato" in <div>.',
'Expected server HTML to contain a matching <div> in <div>.',
);
});

Expand Down
2 changes: 1 addition & 1 deletion src/renderers/native-cs/ReactNativeCSFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ const ReactNativeCSFiber: ReactNativeCSType = {
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = ReactNativeCSFiberRenderer.createContainer(containerTag);
root = ReactNativeCSFiberRenderer.createContainer(containerTag, false);
roots.set(containerTag, root);
}
ReactNativeCSFiberRenderer.updateContainer(element, root, null, callback);
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/native-rt/ReactNativeRTFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const ReactNativeRTFiber: ReactNativeRTType = {
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = ReactNativeRTFiberRenderer.createContainer(containerTag);
root = ReactNativeRTFiberRenderer.createContainer(containerTag, false);
roots.set(containerTag, root);
}
ReactNativeRTFiberRenderer.updateContainer(element, root, null, callback);
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/native/ReactNativeFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const ReactNativeFiber: ReactNativeType = {
if (!root) {
// TODO (bvaughn): If we decide to keep the wrapper component,
// We could create a wrapper for containerTag as well to reduce special casing.
root = ReactNativeFiberRenderer.createContainer(containerTag);
root = ReactNativeFiberRenderer.createContainer(containerTag, false);
roots.set(containerTag, root);
}
ReactNativeFiberRenderer.updateContainer(element, root, null, callback);
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/noop/ReactNoopEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ var ReactNoop = {
if (!root) {
const container = {rootID: rootID, children: []};
rootContainers.set(rootID, container);
root = NoopRenderer.createContainer(container);
root = NoopRenderer.createContainer(container, false);
roots.set(rootID, root);
}
NoopRenderer.updateContainer(element, root, null, callback);
Expand Down
2 changes: 2 additions & 0 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,10 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
return bailoutOnAlreadyFinishedWork(current, workInProgress);
}
const element = state.element;
const root: FiberRoot = workInProgress.stateNode;
if (
(current === null || current.child === null) &&
root.hydrate &&
enterHydrationState(workInProgress)
) {
// If we don't have any current children this might be the first pass.
Expand Down
43 changes: 21 additions & 22 deletions src/renderers/shared/fiber/ReactFiberHydrationContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
didNotMatchHydratedTextInstance,
didNotHydrateContainerInstance,
didNotHydrateInstance,
// TODO: These are currently unused, see below.
// didNotFindHydratableContainerInstance,
// didNotFindHydratableContainerTextInstance,
didNotFindHydratableContainerInstance,
didNotFindHydratableContainerTextInstance,
didNotFindHydratableInstance,
didNotFindHydratableTextInstance,
} = hydration;
Expand Down Expand Up @@ -140,25 +139,25 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
fiber.effectTag |= Placement;
if (__DEV__) {
switch (returnFiber.tag) {
// TODO: Currently we don't warn for insertions into the root because
// we always insert into the root in the non-hydrating case. We just
// delete the existing content. Reenable this once we have a better
// strategy for determining if we're hydrating or not.
// case HostRoot: {
// const parentContainer = returnFiber.stateNode.containerInfo;
// switch (fiber.tag) {
// case HostComponent:
// const type = fiber.type;
// const props = fiber.pendingProps;
// didNotFindHydratableContainerInstance(parentContainer, type, props);
// break;
// case HostText:
// const text = fiber.pendingProps;
// didNotFindHydratableContainerTextInstance(parentContainer, text);
// break;
// }
// break;
// }
case HostRoot: {
const parentContainer = returnFiber.stateNode.containerInfo;
switch (fiber.tag) {
case HostComponent:
const type = fiber.type;
const props = fiber.pendingProps;
didNotFindHydratableContainerInstance(
parentContainer,
type,
props,
);
break;
case HostText:
const text = fiber.pendingProps;
didNotFindHydratableContainerTextInstance(parentContainer, text);
break;
}
break;
}
case HostComponent: {
const parentType = returnFiber.type;
const parentProps = returnFiber.memoizedProps;
Expand Down
6 changes: 3 additions & 3 deletions src/renderers/shared/fiber/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ type HydrationHostConfig<T, P, I, TI, C, CX, PL> = {
};

export type Reconciler<C, I, TI> = {
createContainer(containerInfo: C): OpaqueRoot,
createContainer(containerInfo: C, hydrate: boolean): OpaqueRoot,
updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
Expand Down Expand Up @@ -335,8 +335,8 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}

return {
createContainer(containerInfo: C): OpaqueRoot {
return createFiberRoot(containerInfo);
createContainer(containerInfo: C, hydrate: boolean): OpaqueRoot {
return createFiberRoot(containerInfo, hydrate);
},

updateContainer(
Expand Down
8 changes: 7 additions & 1 deletion src/renderers/shared/fiber/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ export type FiberRoot = {
// Top context object, used by renderSubtreeIntoContainer
context: Object | null,
pendingContext: Object | null,
// Determines if we should attempt to hydrate on the initial mount
+hydrate: boolean,
};

exports.createFiberRoot = function(containerInfo: any): FiberRoot {
exports.createFiberRoot = function(
containerInfo: any,
hydrate: boolean,
): FiberRoot {
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber();
Expand All @@ -39,6 +44,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot {
nextScheduledRoot: null,
context: null,
pendingContext: null,
hydrate,
};
uninitializedFiber.stateNode = root;
return root;
Expand Down
2 changes: 1 addition & 1 deletion src/renderers/testing/ReactTestRendererFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ var ReactTestRendererFiber = {
createNodeMock,
tag: 'CONTAINER',
};
var root: FiberRoot | null = TestRenderer.createContainer(container);
var root: FiberRoot | null = TestRenderer.createContainer(container, false);
invariant(root != null, 'something went wrong');
TestRenderer.updateContainer(element, root, null, null);

Expand Down

0 comments on commit 7687962

Please sign in to comment.