From eef374b3ce0372ed16145094e55747d71579fe7e Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 13 Oct 2017 17:47:06 -0700 Subject: [PATCH] Add hydrate option to createRoot --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 4 +- .../dom/shared/__tests__/ReactDOMRoot-test.js | 36 ++++++++++++++++ .../ReactDOMServerIntegration-test.js | 6 +++ .../__tests__/ReactRenderDocument-test.js | 2 +- .../shared/fiber/ReactFiberBeginWork.js | 2 + .../fiber/ReactFiberHydrationContext.js | 43 +++++++++---------- .../shared/fiber/ReactFiberReconciler.js | 4 +- src/renderers/shared/fiber/ReactFiberRoot.js | 8 +++- 8 files changed, 77 insertions(+), 28 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 68de7ae705604..d319b9d2d8740 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(() => { @@ -769,7 +769,7 @@ type RootOptions = { }; function ReactRoot(container: Container, hydrate: boolean) { - const root = DOMRenderer.createContainer(container); + const root = DOMRenderer.createContainer(container, hydrate); this._reactRootContainer = root; } ReactRoot.prototype.render = function( diff --git a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js index f7a2ddc71f689..c4a327b116eba 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMRoot-test.js @@ -11,6 +11,7 @@ var React = require('react'); var ReactDOM = require('react-dom'); +var ReactDOMServer = require('react-dom/server'); describe('ReactDOMRoot', () => { let container; @@ -32,4 +33,39 @@ describe('ReactDOMRoot', () => { 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 15ea880a94ca4..794a4898b6b2f 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 a68ba409e8e71..1c86f2b5485a6 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/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 59499e01ca057..508a41a86226c 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 c1475017dd761..f6024e39c671f 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 e0972a0314a18..6e31c27a3889d 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -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 f78205d2bcae5..419e842e08be8 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;