From 965217d974d4d040561fa97b9bf4f2f8fa117177 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Tue, 1 Aug 2017 09:57:06 +0100 Subject: [PATCH] Add ReactDOM.hydrate() --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 35 +++++++++++---- .../ReactDOMServerIntegration-test.js | 43 ++++++++++--------- .../__tests__/ReactServerRendering-test.js | 43 +++++++++++-------- .../shared/server/ReactPartialRenderer.js | 1 - 4 files changed, 74 insertions(+), 48 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index e1b338d53d2cd..f2ccde7df8031 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -37,7 +37,7 @@ var { DOCUMENT_NODE, DOCUMENT_FRAGMENT_NODE, } = require('HTMLNodeType'); -var {ID_ATTRIBUTE_NAME} = require('DOMProperty'); +var {ROOT_ATTRIBUTE_NAME} = require('DOMProperty'); var findDOMNode = require('findDOMNode'); var invariant = require('fbjs/lib/invariant'); @@ -127,11 +127,11 @@ function getReactRootElementInContainer(container: any) { } } -function shouldReuseContent(container) { +function shouldHydrateDueToLegacyHeuristic(container) { const rootElement = getReactRootElementInContainer(container); return !!(rootElement && rootElement.nodeType === ELEMENT_NODE && - rootElement.hasAttribute(ID_ATTRIBUTE_NAME)); + rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)); } function shouldAutoFocusHostComponent(type: string, props: Props): boolean { @@ -527,6 +527,7 @@ function renderSubtreeIntoContainer( parentComponent: ?ReactComponent, children: ReactNodeList, container: DOMContainer, + forceHydrate: boolean, callback: ?Function, ) { invariant( @@ -577,9 +578,11 @@ function renderSubtreeIntoContainer( let root = container._reactRootContainer; if (!root) { + const shouldHydrate = + forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // First clear any existing content. - // TODO: Figure out the best heuristic here. - if (!shouldReuseContent(container)) { + // TODO: warn if we're hydrating based on heuristic and suggest using hydrate(). + if (!shouldHydrate) { let warned = false; let rootSibling; while ((rootSibling = container.lastChild)) { @@ -587,7 +590,7 @@ function renderSubtreeIntoContainer( if ( !warned && rootSibling.nodeType === ELEMENT_NODE && - (rootSibling: any).hasAttribute(ID_ATTRIBUTE_NAME) + (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME) ) { warned = true; warning( @@ -614,6 +617,15 @@ function renderSubtreeIntoContainer( } var ReactDOMFiber = { + hydrate( + element: ReactElement, + container: DOMContainer, + callback: ?Function, + ) { + // TODO: throw or warn if we couldn't hydrate? + return renderSubtreeIntoContainer(null, element, container, true, callback); + }, + render( element: ReactElement, container: DOMContainer, @@ -651,7 +663,13 @@ var ReactDOMFiber = { } } } - return renderSubtreeIntoContainer(null, element, container, callback); + return renderSubtreeIntoContainer( + null, + element, + container, + false, + callback, + ); }, unstable_renderSubtreeIntoContainer( @@ -668,6 +686,7 @@ var ReactDOMFiber = { parentComponent, element, containerNode, + false, callback, ); }, @@ -692,7 +711,7 @@ var ReactDOMFiber = { // Unmount should not be batched. DOMRenderer.unbatchedUpdates(() => { - renderSubtreeIntoContainer(null, null, container, () => { + renderSubtreeIntoContainer(null, null, container, false, () => { container._reactRootContainer = null; }); }); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js index 6c7aa87bc5465..5b190d531c879 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js @@ -30,9 +30,13 @@ const COMMENT_NODE_TYPE = 8; // ==================================== // promisified version of ReactDOM.render() -function asyncReactDOMRender(reactElement, domElement) { +function asyncReactDOMRender(reactElement, domElement, forceHydrate) { return new Promise(resolve => { - ReactDOM.render(reactElement, domElement); + if (forceHydrate && ReactDOMFeatureFlags.useFiber) { + ReactDOM.hydrate(reactElement, domElement); + } else { + ReactDOM.render(reactElement, domElement); + } // We can't use the callback for resolution because that will not catch // errors. They're thrown. resolve(); @@ -68,10 +72,10 @@ async function expectErrors(fn, count) { // renders the reactElement into domElement, and expects a certain number of errors. // returns a Promise that resolves when the render is complete. -function renderIntoDom(reactElement, domElement, errorCount = 0) { +function renderIntoDom(reactElement, domElement, forceHydrate, errorCount = 0) { return expectErrors(async () => { ExecutionEnvironment.canUseDOM = true; - await asyncReactDOMRender(reactElement, domElement); + await asyncReactDOMRender(reactElement, domElement, forceHydrate); ExecutionEnvironment.canUseDOM = false; return domElement.firstChild; }, errorCount); @@ -135,7 +139,7 @@ async function streamRender(reactElement, errorCount = 0) { const clientCleanRender = (element, errorCount = 0) => { const div = document.createElement('div'); - return renderIntoDom(element, div, errorCount); + return renderIntoDom(element, div, false, errorCount); }; const clientRenderOnServerString = async (element, errorCount = 0) => { @@ -146,7 +150,12 @@ const clientRenderOnServerString = async (element, errorCount = 0) => { domElement.innerHTML = markup; let serverNode = domElement.firstChild; - const firstClientNode = await renderIntoDom(element, domElement, errorCount); + const firstClientNode = await renderIntoDom( + element, + domElement, + true, + errorCount, + ); let clientNode = firstClientNode; // Make sure all top level nodes match up @@ -154,19 +163,10 @@ const clientRenderOnServerString = async (element, errorCount = 0) => { expect(serverNode != null).toBe(true); expect(clientNode != null).toBe(true); expect(clientNode.nodeType).toBe(serverNode.nodeType); - if (clientNode.nodeType === TEXT_NODE_TYPE) { - // Text nodes are stateless so we can just compare values. - // This works around a current issue where hydration replaces top-level - // text node, but otherwise works. - // TODO: we can remove this branch if we add explicit hydration opt-in. - // https://github.com/facebook/react/issues/10189 - expect(serverNode.nodeValue).toBe(clientNode.nodeValue); - } else { - // Assert that the DOM element hasn't been replaced. - // Note that we cannot use expect(serverNode).toBe(clientNode) because - // of jest bug #1772. - expect(serverNode === clientNode).toBe(true); - } + // Assert that the DOM element hasn't been replaced. + // Note that we cannot use expect(serverNode).toBe(clientNode) because + // of jest bug #1772. + expect(serverNode === clientNode).toBe(true); serverNode = serverNode.nextSibling; clientNode = clientNode.nextSibling; } @@ -180,7 +180,7 @@ const clientRenderOnBadMarkup = async (element, errorCount = 0) => { var domElement = document.createElement('div'); domElement.innerHTML = '
'; - await renderIntoDom(element, domElement, errorCount + 1); + await renderIntoDom(element, domElement, true, errorCount + 1); // This gives us the resulting text content. var hydratedTextContent = domElement.textContent; @@ -188,7 +188,7 @@ const clientRenderOnBadMarkup = async (element, errorCount = 0) => { // Next we render the element into a clean DOM node client side. const cleanDomElement = document.createElement('div'); ExecutionEnvironment.canUseDOM = true; - await asyncReactDOMRender(element, cleanDomElement); + await asyncReactDOMRender(element, cleanDomElement, true); ExecutionEnvironment.canUseDOM = false; // This gives us the expected text content. const cleanTextContent = cleanDomElement.textContent; @@ -296,6 +296,7 @@ async function testMarkupMatch(serverElement, clientElement, shouldMatch) { return renderIntoDom( clientElement, domElement.parentNode, + true, shouldMatch ? 0 : 1, ); } diff --git a/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js b/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js index 8254f12770924..a86c0a6c17c79 100644 --- a/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js +++ b/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js @@ -55,9 +55,10 @@ describe('ReactDOMServer', () => { new RegExp( ' { new RegExp( ' { new RegExp( ' { new RegExp( '
' + - '' + + '' + (ReactDOMFeatureFlags.useFiber ? 'My name is child' : 'My name is ' + @@ -206,9 +212,10 @@ describe('ReactDOMServer', () => { new RegExp( '