diff --git a/src/renderers/__tests__/ReactCompositeComponent-test.js b/src/renderers/__tests__/ReactCompositeComponent-test.js index 2f69aa9ebcd4b..650157f967f8c 100644 --- a/src/renderers/__tests__/ReactCompositeComponent-test.js +++ b/src/renderers/__tests__/ReactCompositeComponent-test.js @@ -15,6 +15,7 @@ var ChildUpdates; var MorphingComponent; var React; var ReactDOM; +var ReactDOMFeatureFlags; var ReactDOMServer; var ReactCurrentOwner; var ReactTestUtils; @@ -27,6 +28,7 @@ describe('ReactCompositeComponent', () => { jest.resetModules(); React = require('react'); ReactDOM = require('react-dom'); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); ReactDOMServer = require('react-dom/server'); ReactCurrentOwner = require('react') .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner; @@ -116,11 +118,32 @@ describe('ReactCompositeComponent', () => { } } + spyOn(console, 'warn'); var markup = ReactDOMServer.renderToString(); + + // Old API based on heuristic var container = document.createElement('div'); container.innerHTML = markup; - ReactDOM.render(, container); + if (ReactDOMFeatureFlags.useFiber) { + expectDev(console.warn.calls.count()).toBe(1); + expectDev(console.warn.calls.argsFor(0)[0]).toContain( + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + + 'will stop working in React v17. Replace the ReactDOM.render() call ' + + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', + ); + } else { + expectDev(console.warn.calls.count()).toBe(0); + } + + // New explicit API + console.warn.calls.reset(); + if (ReactDOMFeatureFlags.useFiber) { + container = document.createElement('div'); + container.innerHTML = markup; + ReactDOM.hydrate(, container); + expectDev(console.warn.calls.count()).toBe(0); + } }); it('should react to state changes from callbacks', () => { diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index e1b338d53d2cd..c9386768b0848 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'); @@ -58,6 +58,7 @@ var { var {precacheFiberNode, updateFiberProps} = ReactDOMComponentTree; if (__DEV__) { + var lowPriorityWarning = require('lowPriorityWarning'); var warning = require('fbjs/lib/warning'); var validateDOMNesting = require('validateDOMNesting'); var {updatedAncestorInfo} = validateDOMNesting; @@ -127,11 +128,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 { @@ -523,10 +524,14 @@ ReactGenericBatching.injection.injectFiberBatchedUpdates( DOMRenderer.batchedUpdates, ); +var warnedAboutHydrateAPI = false; +var warnedAboutEmptyContainer = false; + function renderSubtreeIntoContainer( parentComponent: ?ReactComponent, children: ReactNodeList, container: DOMContainer, + forceHydrate: boolean, callback: ?Function, ) { invariant( @@ -577,9 +582,10 @@ 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)) { + if (!shouldHydrate) { let warned = false; let rootSibling; while ((rootSibling = container.lastChild)) { @@ -587,7 +593,7 @@ function renderSubtreeIntoContainer( if ( !warned && rootSibling.nodeType === ELEMENT_NODE && - (rootSibling: any).hasAttribute(ID_ATTRIBUTE_NAME) + (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME) ) { warned = true; warning( @@ -601,6 +607,25 @@ function renderSubtreeIntoContainer( container.removeChild(rootSibling); } } + if (__DEV__) { + if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) { + warnedAboutHydrateAPI = true; + lowPriorityWarning( + false, + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + + 'will stop working in React v17. Replace the ReactDOM.render() call ' + + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', + ); + } + if (forceHydrate && !container.firstChild && !warnedAboutEmptyContainer) { + warnedAboutEmptyContainer = true; + warning( + false, + 'hydrate(): Expected to hydrate from server-rendered markup, but the passed ' + + 'DOM container node was empty. React will create the DOM from scratch.', + ); + } + } const newRoot = DOMRenderer.createContainer(container); root = container._reactRootContainer = newRoot; // Initial mount should not be batched. @@ -614,6 +639,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 +685,13 @@ var ReactDOMFiber = { } } } - return renderSubtreeIntoContainer(null, element, container, callback); + return renderSubtreeIntoContainer( + null, + element, + container, + false, + callback, + ); }, unstable_renderSubtreeIntoContainer( @@ -668,6 +708,7 @@ var ReactDOMFiber = { parentComponent, element, containerNode, + false, callback, ); }, @@ -692,7 +733,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__/ReactDOMComponentTree-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js index 369420522bb45..fd1b463d5f0e7 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js @@ -11,6 +11,8 @@ 'use strict'; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + describe('ReactDOMComponentTree', () => { var React; var ReactDOM; @@ -21,7 +23,11 @@ describe('ReactDOMComponentTree', () => { var container = document.createElement('div'); // Force server-rendering path: container.innerHTML = ReactDOMServer.renderToString(elt); - return ReactDOM.render(elt, container); + if (ReactDOMFeatureFlags.useFiber) { + return ReactDOM.hydrate(elt, container); + } else { + return ReactDOM.render(elt, container); + } } function getTypeOf(instance) { diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js index 6c7aa87bc5465..8654506b56282 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, ); } @@ -1971,7 +1972,11 @@ describe('ReactDOMServerIntegration', () => { resetModules(); // client render on top of the server markup. - const clientField = await renderIntoDom(element, field.parentNode); + const clientField = await renderIntoDom( + element, + field.parentNode, + true, + ); // verify that the input field was not replaced. // Note that we cannot use expect(clientField).toBe(field) because // of jest bug #1772 @@ -2330,6 +2335,7 @@ describe('ReactDOMServerIntegration', () => { await asyncReactDOMRender( (component = e)} />, root, + true, ); expect(component.refs.myDiv).toBe(root.firstChild); }); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js index 013eabaf04a1b..28a2196a0209f 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js @@ -14,6 +14,7 @@ var React; var ReactDOM; var ReactDOMServer; +var ReactDOMFeatureFlags; // In standard React, TextComponent keeps track of different Text templates // using comments. However, in React Fiber, those comments are not outputted due @@ -29,6 +30,7 @@ describe('ReactDOMTextComponent', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); }); it('updates a mounted text component in place', () => { @@ -117,7 +119,11 @@ describe('ReactDOMTextComponent', () => { var reactEl =
{'foo'}{'bar'}{'baz'}
; el.innerHTML = ReactDOMServer.renderToString(reactEl); - ReactDOM.render(reactEl, el); + if (ReactDOMFeatureFlags.useFiber) { + ReactDOM.hydrate(reactEl, el); + } else { + ReactDOM.render(reactEl, el); + } expect(el.textContent).toBe('foobarbaz'); ReactDOM.unmountComponentAtNode(el); @@ -125,7 +131,11 @@ describe('ReactDOMTextComponent', () => { reactEl =
{''}{''}{''}
; el.innerHTML = ReactDOMServer.renderToString(reactEl); - ReactDOM.render(reactEl, el); + if (ReactDOMFeatureFlags.useFiber) { + ReactDOM.hydrate(reactEl, el); + } else { + ReactDOM.render(reactEl, el); + } expect(el.textContent).toBe(''); }); diff --git a/src/renderers/dom/shared/__tests__/ReactMount-test.js b/src/renderers/dom/shared/__tests__/ReactMount-test.js index a522675d74e10..458cbd09ff909 100644 --- a/src/renderers/dom/shared/__tests__/ReactMount-test.js +++ b/src/renderers/dom/shared/__tests__/ReactMount-test.js @@ -146,12 +146,16 @@ describe('ReactMount', () => { expect(instance1 === instance2).toBe(true); }); - it('should warn if mounting into dirty rendered markup', () => { + it('should warn if mounting into left padded rendered markup', () => { var container = document.createElement('container'); container.innerHTML = ReactDOMServer.renderToString(
) + ' '; spyOn(console, 'error'); - ReactDOM.render(
, container); + if (ReactDOMFeatureFlags.useFiber) { + ReactDOM.hydrate(
, container); + } else { + ReactDOM.render(
, container); + } expectDev(console.error.calls.count()).toBe(1); if (ReactDOMFeatureFlags.useFiber) { expectDev(console.error.calls.argsFor(0)[0]).toContain( @@ -162,15 +166,28 @@ describe('ReactMount', () => { 'Target node has markup rendered by React, but there are unrelated nodes as well.', ); } + }); - console.error.calls.reset(); - ReactDOM.unmountComponentAtNode(container); + it('should warn if mounting into right padded rendered markup', () => { + var container = document.createElement('container'); container.innerHTML = ' ' + ReactDOMServer.renderToString(
); - ReactDOM.render(
, container); + + spyOn(console, 'error'); + if (ReactDOMFeatureFlags.useFiber) { + ReactDOM.hydrate(
, container); + } else { + ReactDOM.render(
, container); + } expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toContain( - 'Target node has markup rendered by React, but there are unrelated nodes as well.', - ); + if (ReactDOMFeatureFlags.useFiber) { + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Did not expect server HTML to contain the text node " " in .', + ); + } else { + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Target node has markup rendered by React, but there are unrelated nodes as well.', + ); + } }); it('should not warn if mounting into non-empty node', () => { @@ -203,10 +220,17 @@ describe('ReactMount', () => { div.innerHTML = markup; spyOn(console, 'error'); - ReactDOM.render( -
This markup contains an nbsp entity:   client text
, - div, - ); + if (ReactDOMFeatureFlags.useFiber) { + ReactDOM.hydrate( +
This markup contains an nbsp entity:   client text
, + div, + ); + } else { + ReactDOM.render( +
This markup contains an nbsp entity:   client text
, + div, + ); + } expectDev(console.error.calls.count()).toBe(1); if (ReactDOMFeatureFlags.useFiber) { expectDev(console.error.calls.argsFor(0)[0]).toContain( diff --git a/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js b/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js index 21089d763821d..89eccba3bc25c 100644 --- a/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js +++ b/src/renderers/dom/shared/__tests__/ReactRenderDocument-test.js @@ -14,10 +14,11 @@ var React; var ReactDOM; var ReactDOMServer; -var ReactDOMFeatureFlags; var getTestDocument; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + var UNMOUNT_INVARIANT_MESSAGE = ' tried to unmount. ' + 'Because of cross-browser quirks it is impossible to unmount some ' + @@ -32,256 +33,524 @@ describe('rendering React components at document', () => { React = require('react'); ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); - ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); getTestDocument = require('getTestDocument'); }); - it('should be able to adopt server markup', () => { - class Root extends React.Component { - render() { - return ( - - - Hello World - - - {'Hello ' + this.props.hello} - - + describe('with old implicit hydration API', () => { + function expectDeprecationWarningWithFiber() { + if (ReactDOMFeatureFlags.useFiber) { + expectDev(console.warn.calls.count()).toBe(1); + expectDev(console.warn.calls.argsFor(0)[0]).toContain( + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + + 'will stop working in React v17. Replace the ReactDOM.render() call ' + + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', ); + } else { + expectDev(console.warn.calls.count()).toBe(0); } } - var markup = ReactDOMServer.renderToString(); - var testDocument = getTestDocument(markup); - var body = testDocument.body; - - ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello world'); + it('should be able to adopt server markup', () => { + spyOn(console, 'warn'); + class Root extends React.Component { + render() { + return ( + + + Hello World + + + {'Hello ' + this.props.hello} + + + ); + } + } - ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello moon'); + var markup = ReactDOMServer.renderToString(); + var testDocument = getTestDocument(markup); + var body = testDocument.body; - expect(body === testDocument.body).toBe(true); - }); + ReactDOM.render(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); - it('should not be able to unmount component from document node', () => { - class Root extends React.Component { - render() { - return ( - - - Hello World - - - Hello world - - - ); + ReactDOM.render(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello moon'); + + expect(body === testDocument.body).toBe(true); + expectDeprecationWarningWithFiber(); + }); + + it('should not be able to unmount component from document node', () => { + spyOn(console, 'warn'); + class Root extends React.Component { + render() { + return ( + + + Hello World + + + Hello world + + + ); + } } - } - - var markup = ReactDOMServer.renderToString(); - var testDocument = getTestDocument(markup); - ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello world'); - - if (ReactDOMFeatureFlags.useFiber) { - // In Fiber this actually works. It might not be a good idea though. - ReactDOM.unmountComponentAtNode(testDocument); - expect(testDocument.firstChild).toBe(null); - } else { - expect(function() { - ReactDOM.unmountComponentAtNode(testDocument); - }).toThrowError(UNMOUNT_INVARIANT_MESSAGE); + var markup = ReactDOMServer.renderToString(); + var testDocument = getTestDocument(markup); + ReactDOM.render(, testDocument); expect(testDocument.body.innerHTML).toBe('Hello world'); - } - }); - it('should not be able to switch root constructors', () => { - class Component extends React.Component { - render() { - return ( - - - Hello World - - - Hello world - - - ); + if (ReactDOMFeatureFlags.useFiber) { + // In Fiber this actually works. It might not be a good idea though. + ReactDOM.unmountComponentAtNode(testDocument); + expect(testDocument.firstChild).toBe(null); + } else { + expect(function() { + ReactDOM.unmountComponentAtNode(testDocument); + }).toThrowError(UNMOUNT_INVARIANT_MESSAGE); + + expect(testDocument.body.innerHTML).toBe('Hello world'); } - } - class Component2 extends React.Component { - render() { - return ( - - - Hello World - - - Goodbye world - - - ); + expectDeprecationWarningWithFiber(); + }); + + it('should not be able to switch root constructors', () => { + spyOn(console, 'warn'); + class Component extends React.Component { + render() { + return ( + + + Hello World + + + Hello world + + + ); + } } - } - var markup = ReactDOMServer.renderToString(); - var testDocument = getTestDocument(markup); + class Component2 extends React.Component { + render() { + return ( + + + Hello World + + + Goodbye world + + + ); + } + } - ReactDOM.render(, testDocument); + var markup = ReactDOMServer.renderToString(); + var testDocument = getTestDocument(markup); - expect(testDocument.body.innerHTML).toBe('Hello world'); + ReactDOM.render(, testDocument); - // Reactive update - if (ReactDOMFeatureFlags.useFiber) { - // This works but is probably a bad idea. - ReactDOM.render(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); - expect(testDocument.body.innerHTML).toBe('Goodbye world'); - } else { - expect(function() { + // Reactive update + if (ReactDOMFeatureFlags.useFiber) { + // This works but is probably a bad idea. ReactDOM.render(, testDocument); - }).toThrowError(UNMOUNT_INVARIANT_MESSAGE); - expect(testDocument.body.innerHTML).toBe('Hello world'); - } - }); + expect(testDocument.body.innerHTML).toBe('Goodbye world'); + } else { + expect(function() { + ReactDOM.render(, testDocument); + }).toThrowError(UNMOUNT_INVARIANT_MESSAGE); - it('should be able to mount into document', () => { - class Component extends React.Component { - render() { - return ( - - - Hello World - - - {this.props.text} - - - ); + expect(testDocument.body.innerHTML).toBe('Hello world'); } - } - var markup = ReactDOMServer.renderToString( - , - ); - var testDocument = getTestDocument(markup); + expectDeprecationWarningWithFiber(); + }); + + it('should be able to mount into document', () => { + spyOn(console, 'warn'); + class Component extends React.Component { + render() { + return ( + + + Hello World + + + {this.props.text} + + + ); + } + } - ReactDOM.render(, testDocument); + var markup = ReactDOMServer.renderToString( + , + ); + var testDocument = getTestDocument(markup); - expect(testDocument.body.innerHTML).toBe('Hello world'); - }); + ReactDOM.render(, testDocument); - it('renders over an existing text child without throwing', () => { - const container = document.createElement('div'); - container.textContent = 'potato'; - ReactDOM.render(
parsnip
, container); - expect(container.textContent).toBe('parsnip'); - }); + expect(testDocument.body.innerHTML).toBe('Hello world'); + expectDeprecationWarningWithFiber(); + }); + + it('renders over an existing text child without throwing', () => { + const container = document.createElement('div'); + container.textContent = 'potato'; + ReactDOM.render(
parsnip
, container); + expect(container.textContent).toBe('parsnip'); + // We don't expect a warning about new hydration API here because + // we aren't sure if the user meant to hydrate or replace a stub node. + // We would see a warning if the container had React-rendered HTML in it. + }); + + it('should give helpful errors on state desync', () => { + class Component extends React.Component { + render() { + return ( + + + Hello World + + + {this.props.text} + + + ); + } + } - it('should give helpful errors on state desync', () => { - class Component extends React.Component { - render() { - return ( - - - Hello World - - - {this.props.text} - - + var markup = ReactDOMServer.renderToString( + , + ); + var testDocument = getTestDocument(markup); + + if (ReactDOMFeatureFlags.useFiber) { + spyOn(console, 'warn'); + spyOn(console, 'error'); + ReactDOM.render(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + expectDev(console.warn.calls.count()).toBe(1); + expectDev(console.warn.calls.argsFor(0)[0]).toContain( + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + + 'will stop working in React v17. Replace the ReactDOM.render() call ' + + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', + ); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Text content did not match.', + ); + } else { + expect(function() { + // Notice the text is different! + ReactDOM.render(, testDocument); + }).toThrowError( + "You're trying to render a component to the document using " + + 'server rendering but the checksum was invalid. This usually ' + + 'means you rendered a different component type or props on ' + + 'the client from the one on the server, or your render() methods ' + + 'are impure. React cannot handle this case due to cross-browser ' + + 'quirks by rendering at the document root. You should look for ' + + 'environment dependent code in your components and ensure ' + + 'the props are the same client and server side:\n' + + ' (client) dy data-reactid="4">Hello worldGoodbye world', ); } - } - - var markup = ReactDOMServer.renderToString( - , - ); - var testDocument = getTestDocument(markup); + }); + + it('should throw on full document render w/ no markup', () => { + var testDocument = getTestDocument(); + + class Component extends React.Component { + render() { + return ( + + + Hello World + + + {this.props.text} + + + ); + } + } - if (ReactDOMFeatureFlags.useFiber) { - spyOn(console, 'error'); - ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello world'); - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toContain( - 'Warning: Text content did not match.', - ); - } else { - expect(function() { - // Notice the text is different! + if (ReactDOMFeatureFlags.useFiber) { ReactDOM.render(, testDocument); - }).toThrowError( - "You're trying to render a component to the document using " + - 'server rendering but the checksum was invalid. This usually ' + - 'means you rendered a different component type or props on ' + - 'the client from the one on the server, or your render() methods ' + - 'are impure. React cannot handle this case due to cross-browser ' + - 'quirks by rendering at the document root. You should look for ' + - 'environment dependent code in your components and ensure ' + - 'the props are the same client and server side:\n' + - ' (client) dy data-reactid="4">Hello worldGoodbye world', + expect(testDocument.body.innerHTML).toBe('Hello world'); + } else { + expect(function() { + ReactDOM.render(, testDocument); + }).toThrowError( + "You're trying to render a component to the document but you didn't " + + "use server rendering. We can't do this without using server " + + 'rendering due to cross-browser quirks. See ' + + 'ReactDOMServer.renderToString() for server rendering.', + ); + } + // We don't expect a warning about new hydration API here because + // we aren't sure if the user meant to hydrate or replace the document. + // We would see a warning if the document had React-rendered HTML in it. + }); + + it('supports findDOMNode on full-page components', () => { + spyOn(console, 'warn'); + var tree = ( + + + Hello World + + + Hello world + + ); - } + + var markup = ReactDOMServer.renderToString(tree); + var testDocument = getTestDocument(markup); + var component = ReactDOM.render(tree, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML'); + expectDeprecationWarningWithFiber(); + }); }); - it('should throw on full document render w/ no markup', () => { - var testDocument = getTestDocument(); + if (ReactDOMFeatureFlags.useFiber) { + describe('with new explicit hydration API', () => { + it('warns if there is no server rendered markup to hydrate', () => { + spyOn(console, 'error'); + const container = document.createElement('div'); + ReactDOM.hydrate(
, container); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'hydrate(): Expected to hydrate from server-rendered markup, but the passed ' + + 'DOM container node was empty. React will create the DOM from scratch.', + ); + }); + + it('should be able to adopt server markup', () => { + class Root extends React.Component { + render() { + return ( + + + Hello World + + + {'Hello ' + this.props.hello} + + + ); + } + } + + var markup = ReactDOMServer.renderToString(); + var testDocument = getTestDocument(markup); + var body = testDocument.body; + + ReactDOM.hydrate(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + + ReactDOM.hydrate(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello moon'); + + expect(body === testDocument.body).toBe(true); + }); + + it('should not be able to unmount component from document node', () => { + class Root extends React.Component { + render() { + return ( + + + Hello World + + + Hello world + + + ); + } + } + + var markup = ReactDOMServer.renderToString(); + var testDocument = getTestDocument(markup); + ReactDOM.hydrate(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + + // In Fiber this actually works. It might not be a good idea though. + ReactDOM.unmountComponentAtNode(testDocument); + expect(testDocument.firstChild).toBe(null); + }); + + it('should not be able to switch root constructors', () => { + class Component extends React.Component { + render() { + return ( + + + Hello World + + + Hello world + + + ); + } + } + + class Component2 extends React.Component { + render() { + return ( + + + Hello World + + + Goodbye world + + + ); + } + } + + var markup = ReactDOMServer.renderToString(); + var testDocument = getTestDocument(markup); + + ReactDOM.hydrate(, testDocument); + + expect(testDocument.body.innerHTML).toBe('Hello world'); + + // This works but is probably a bad idea. + ReactDOM.hydrate(, testDocument); + + expect(testDocument.body.innerHTML).toBe('Goodbye world'); + }); + + it('should be able to mount into document', () => { + class Component extends React.Component { + render() { + return ( + + + Hello World + + + {this.props.text} + + + ); + } + } + + var markup = ReactDOMServer.renderToString( + , + ); + var testDocument = getTestDocument(markup); + + ReactDOM.hydrate(, testDocument); + + expect(testDocument.body.innerHTML).toBe('Hello world'); + }); + + it('renders over an existing text child without throwing', () => { + spyOn(console, 'error'); + const container = document.createElement('div'); + container.textContent = 'potato'; + ReactDOM.hydrate(
parsnip
, container); + 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
.', + ); + }); + + it('should give helpful errors on state desync', () => { + class Component extends React.Component { + render() { + return ( + + + Hello World + + + {this.props.text} + + + ); + } + } + + var markup = ReactDOMServer.renderToString( + , + ); + var testDocument = getTestDocument(markup); + + spyOn(console, 'error'); + ReactDOM.hydrate(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Text content did not match.', + ); + }); + + it('should render w/ no markup to full document', () => { + spyOn(console, 'error'); + var testDocument = getTestDocument(); + + class Component extends React.Component { + render() { + return ( + + + Hello World + + + {this.props.text} + + + ); + } + } + + ReactDOM.hydrate(, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + // getTestDocument() has an extra that we didn't render. + 'Did not expect server HTML to contain a in .', + ); + }); - class Component extends React.Component { - render() { - return ( + it('supports findDOMNode on full-page components', () => { + var tree = ( Hello World - {this.props.text} + Hello world ); - } - } - if (ReactDOMFeatureFlags.useFiber) { - ReactDOM.render(, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello world'); - } else { - expect(function() { - ReactDOM.render(, testDocument); - }).toThrowError( - "You're trying to render a component to the document but you didn't " + - "use server rendering. We can't do this without using server " + - 'rendering due to cross-browser quirks. See ' + - 'ReactDOMServer.renderToString() for server rendering.', - ); - } - }); - - it('supports findDOMNode on full-page components', () => { - var tree = ( - - - Hello World - - - Hello world - - - ); - - var markup = ReactDOMServer.renderToString(tree); - var testDocument = getTestDocument(markup); - var component = ReactDOM.render(tree, testDocument); - expect(testDocument.body.innerHTML).toBe('Hello world'); - expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML'); - }); + var markup = ReactDOMServer.renderToString(tree); + var testDocument = getTestDocument(markup); + var component = ReactDOM.hydrate(tree, testDocument); + expect(testDocument.body.innerHTML).toBe('Hello world'); + expect(ReactDOM.findDOMNode(component).tagName).toBe('HTML'); + }); + }); + } }); diff --git a/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js b/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js index 5834b828d2da8..60ea2e7b2c80d 100644 --- a/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js +++ b/src/renderers/dom/shared/__tests__/ReactServerRendering-test.js @@ -14,7 +14,6 @@ var ExecutionEnvironment; var React; var ReactDOM; -var ReactDOMFeatureFlags; var ReactDOMServer; var ReactMarkupChecksum; var ReactReconcileTransaction; @@ -22,6 +21,8 @@ var ReactTestUtils; var PropTypes; var ReactFeatureFlags; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + var ID_ATTRIBUTE_NAME; var ROOT_ATTRIBUTE_NAME; @@ -31,7 +32,6 @@ describe('ReactDOMServer', () => { React = require('react'); ReactDOM = require('react-dom'); ReactTestUtils = require('react-dom/test-utils'); - ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); ReactMarkupChecksum = require('ReactMarkupChecksum'); ReactReconcileTransaction = require('ReactReconcileTransaction'); PropTypes = require('prop-types'); @@ -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( ' { runTest(); }); - it('should have the correct mounting behavior', () => { + it('should have the correct mounting behavior (old hydrate API)', () => { + spyOn(console, 'warn'); + spyOn(console, 'error'); // This test is testing client-side behavior. ExecutionEnvironment.canUseDOM = true; @@ -289,6 +298,17 @@ describe('ReactDOMServer', () => { var instance = ReactDOM.render(, element); expect(mountCount).toEqual(3); + if (ReactDOMFeatureFlags.useFiber) { + expectDev(console.warn.calls.count()).toBe(1); + expectDev(console.warn.calls.argsFor(0)[0]).toContain( + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' + + 'will stop working in React v17. Replace the ReactDOM.render() call ' + + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.', + ); + } else { + expectDev(console.warn.calls.count()).toBe(0); + } + console.warn.calls.reset(); var expectedMarkup = lastMarkup; if (ReactDOMFeatureFlags.useFiber) { @@ -308,7 +328,6 @@ describe('ReactDOMServer', () => { // Now simulate a situation where the app is not idempotent. React should // warn but do the right thing. element.innerHTML = lastMarkup; - spyOn(console, 'error'); instance = ReactDOM.render(, element); expect(mountCount).toEqual(4); expectDev(console.error.calls.count()).toBe(1); @@ -324,6 +343,7 @@ describe('ReactDOMServer', () => { '(server) -- react-text: 3 -->x/g; + expectedMarkup = expectedMarkup.replace(reactComments, ''); + } + expect(element.innerHTML).toBe(expectedMarkup); + + // Ensure the events system works after mount into server markup + expect(numClicks).toEqual(0); + ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(instance.refs.span)); + expect(numClicks).toEqual(1); + + ReactDOM.unmountComponentAtNode(element); + expect(element.innerHTML).toEqual(''); + + // Now simulate a situation where the app is not idempotent. React should + // warn but do the right thing. + element.innerHTML = lastMarkup; + instance = ReactDOM.hydrate(, element); + expect(mountCount).toEqual(4); + expectDev(console.error.calls.count()).toBe(1); + expect(element.innerHTML.length > 0).toBe(true); + expect(element.innerHTML).not.toEqual(lastMarkup); + + // Ensure the events system works after markup mismatch. + expect(numClicks).toEqual(1); + ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(instance.refs.span)); + expect(numClicks).toEqual(2); + }); + } + it('should throw with silly args', () => { expect( ReactDOMServer.renderToString.bind(ReactDOMServer, {x: 123}), diff --git a/src/renderers/dom/test/__tests__/ReactTestUtils-test.js b/src/renderers/dom/test/__tests__/ReactTestUtils-test.js index 09337a0caea6d..f04a96da18e22 100644 --- a/src/renderers/dom/test/__tests__/ReactTestUtils-test.js +++ b/src/renderers/dom/test/__tests__/ReactTestUtils-test.js @@ -14,6 +14,7 @@ let createRenderer; let React; let ReactDOM; +let ReactDOMFeatureFlags; let ReactDOMServer; let ReactTestUtils; @@ -24,6 +25,7 @@ describe('ReactTestUtils', () => { ReactDOM = require('react-dom'); ReactDOMServer = require('react-dom/server'); ReactTestUtils = require('react-dom/test-utils'); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); }); it('can scryRenderedDOMComponentsWithClass with TextComponent', () => { @@ -173,7 +175,9 @@ describe('ReactTestUtils', () => { const markup = ReactDOMServer.renderToString(); const testDocument = getTestDocument(markup); - const component = ReactDOM.render(, testDocument); + const component = ReactDOMFeatureFlags.useFiber + ? ReactDOM.hydrate(, testDocument) + : ReactDOM.render(, testDocument); expect(component.refs.html.tagName).toBe('HTML'); expect(component.refs.head.tagName).toBe('HEAD'); diff --git a/src/renderers/shared/server/ReactPartialRenderer.js b/src/renderers/shared/server/ReactPartialRenderer.js index 0149c27eb43fc..148030a4a15f6 100644 --- a/src/renderers/shared/server/ReactPartialRenderer.js +++ b/src/renderers/shared/server/ReactPartialRenderer.js @@ -305,7 +305,6 @@ function createOpenTagMarkup( if (isRootElement) { ret += ' ' + DOMMarkupOperations.createMarkupForRoot(); } - ret += ' ' + DOMMarkupOperations.createMarkupForID(''); return ret; }