Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ReactDOM.createRoot #11225

Merged
merged 2 commits into from
Oct 14, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is not a destroy method now? It's possible to render again after an unmount. With deterministic updates, is the motivation for the new API as strong?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, should probably throw? I'll address in a follow-up.


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