diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 1bdb813b1ef..d319b9d2d87 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -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(() => { @@ -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( diff --git a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js new file mode 100644 index 00000000000..c4a327b116e --- /dev/null +++ b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js @@ -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(
Hi
); + expect(container.textContent).toEqual('Hi'); + }); + + it('unmounts children', () => { + const root = ReactDOM.createRoot(container); + root.render(
Hi
); + expect(container.textContent).toEqual('Hi'); + root.unmount(); + expect(container.textContent).toEqual(''); + }); + + it('supports hydration', async () => { + const markup = await new Promise(resolve => + resolve( + ReactDOMServer.renderToString(
), + ), + ); + + spyOn(console, 'error'); + + // Does not hydrate by default + const container1 = document.createElement('div'); + container1.innerHTML = markup; + const root1 = ReactDOM.createRoot(container1); + root1.render(
); + 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(
); + 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 = '
a
b
'; + const root = ReactDOM.createRoot(container); + root.render(
cd
); + expect(container.textContent).toEqual('abcd'); + root.render(
dc
); + expect(container.textContent).toEqual('abdc'); + }); +}); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js index 15ea880a94c..794a4898b6b 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js @@ -2638,6 +2638,12 @@ describe('ReactDOMServerIntegration', () => { it('should error reconnecting different element types', () => expectMarkupMismatch(
, )); + it('should error reconnecting fewer root children', () => + expectMarkupMismatch(, [ + , + , + ])); + it('should error reconnecting missing attributes', () => expectMarkupMismatch(
,
)); diff --git a/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js b/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js index a68ba409e8e..1c86f2b5485 100644 --- a/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js +++ b/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js @@ -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
.', + 'Expected server HTML to contain a matching
in
.', ); }); diff --git a/src/renderers/native-cs/ReactNativeCSFiberEntry.js b/src/renderers/native-cs/ReactNativeCSFiberEntry.js index 72ceb060c70..66c7cdc0adb 100644 --- a/src/renderers/native-cs/ReactNativeCSFiberEntry.js +++ b/src/renderers/native-cs/ReactNativeCSFiberEntry.js @@ -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); diff --git a/src/renderers/native-rt/ReactNativeRTFiberEntry.js b/src/renderers/native-rt/ReactNativeRTFiberEntry.js index a367e32336c..684b6a7154c 100644 --- a/src/renderers/native-rt/ReactNativeRTFiberEntry.js +++ b/src/renderers/native-rt/ReactNativeRTFiberEntry.js @@ -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); diff --git a/src/renderers/native/ReactNativeFiberEntry.js b/src/renderers/native/ReactNativeFiberEntry.js index b5ba64dbd3f..e149c74a7fa 100644 --- a/src/renderers/native/ReactNativeFiberEntry.js +++ b/src/renderers/native/ReactNativeFiberEntry.js @@ -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); diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index a1b7cf099a0..89bfc33c2d7 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -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); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 59499e01ca0..508a41a8622 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -338,8 +338,10 @@ module.exports = function( 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. diff --git a/src/renderers/shared/fiber/ReactFiberHydrationContext.js b/src/renderers/shared/fiber/ReactFiberHydrationContext.js index c1475017dd7..f6024e39c67 100644 --- a/src/renderers/shared/fiber/ReactFiberHydrationContext.js +++ b/src/renderers/shared/fiber/ReactFiberHydrationContext.js @@ -77,9 +77,8 @@ module.exports = function( didNotMatchHydratedTextInstance, didNotHydrateContainerInstance, didNotHydrateInstance, - // TODO: These are currently unused, see below. - // didNotFindHydratableContainerInstance, - // didNotFindHydratableContainerTextInstance, + didNotFindHydratableContainerInstance, + didNotFindHydratableContainerTextInstance, didNotFindHydratableInstance, didNotFindHydratableTextInstance, } = hydration; @@ -140,25 +139,25 @@ module.exports = function( 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; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index e0972a0314a..7f6a7c3017c 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -219,7 +219,7 @@ type HydrationHostConfig = { }; export type Reconciler = { - createContainer(containerInfo: C): OpaqueRoot, + createContainer(containerInfo: C, hydrate: boolean): OpaqueRoot, updateContainer( element: ReactNodeList, container: OpaqueRoot, @@ -335,8 +335,8 @@ module.exports = function( } return { - createContainer(containerInfo: C): OpaqueRoot { - return createFiberRoot(containerInfo); + createContainer(containerInfo: C, hydrate: boolean): OpaqueRoot { + return createFiberRoot(containerInfo, hydrate); }, updateContainer( diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index f78205d2bca..419e842e08b 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -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(); @@ -39,6 +44,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { nextScheduledRoot: null, context: null, pendingContext: null, + hydrate, }; uninitializedFiber.stateNode = root; return root; diff --git a/src/renderers/testing/ReactTestRendererFiberEntry.js b/src/renderers/testing/ReactTestRendererFiberEntry.js index 1b0253f5125..79144a1449a 100644 --- a/src/renderers/testing/ReactTestRendererFiberEntry.js +++ b/src/renderers/testing/ReactTestRendererFiberEntry.js @@ -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);