Skip to content

Commit

Permalink
Add ReactDOM.hydrate()
Browse files Browse the repository at this point in the history
  • Loading branch information
gaearon committed Aug 1, 2017
1 parent 04db351 commit 965217d
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 48 deletions.
35 changes: 27 additions & 8 deletions src/renderers/dom/fiber/ReactDOMFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -527,6 +527,7 @@ function renderSubtreeIntoContainer(
parentComponent: ?ReactComponent<any, any, any>,
children: ReactNodeList,
container: DOMContainer,
forceHydrate: boolean,
callback: ?Function,
) {
invariant(
Expand Down Expand Up @@ -577,17 +578,19 @@ 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)) {
if (__DEV__) {
if (
!warned &&
rootSibling.nodeType === ELEMENT_NODE &&
(rootSibling: any).hasAttribute(ID_ATTRIBUTE_NAME)
(rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
) {
warned = true;
warning(
Expand All @@ -614,6 +617,15 @@ function renderSubtreeIntoContainer(
}

var ReactDOMFiber = {
hydrate(
element: ReactElement<any>,
container: DOMContainer,
callback: ?Function,
) {
// TODO: throw or warn if we couldn't hydrate?
return renderSubtreeIntoContainer(null, element, container, true, callback);
},

render(
element: ReactElement<any>,
container: DOMContainer,
Expand Down Expand Up @@ -651,7 +663,13 @@ var ReactDOMFiber = {
}
}
}
return renderSubtreeIntoContainer(null, element, container, callback);
return renderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},

unstable_renderSubtreeIntoContainer(
Expand All @@ -668,6 +686,7 @@ var ReactDOMFiber = {
parentComponent,
element,
containerNode,
false,
callback,
);
},
Expand All @@ -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;
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) => {
Expand All @@ -146,27 +150,23 @@ 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
while (serverNode || clientNode) {
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;
}
Expand All @@ -180,15 +180,15 @@ const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
var domElement = document.createElement('div');
domElement.innerHTML =
'<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
await renderIntoDom(element, domElement, errorCount + 1);
await renderIntoDom(element, domElement, true, errorCount + 1);

// This gives us the resulting text content.
var hydratedTextContent = domElement.textContent;

// 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;
Expand Down Expand Up @@ -296,6 +296,7 @@ async function testMarkupMatch(serverElement, clientElement, shouldMatch) {
return renderIntoDom(
clientElement,
domElement.parentNode,
true,
shouldMatch ? 0 : 1,
);
}
Expand Down
43 changes: 25 additions & 18 deletions src/renderers/dom/shared/__tests__/ReactServerRendering-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ describe('ReactDOMServer', () => {
new RegExp(
'<span ' +
ROOT_ATTRIBUTE_NAME +
'="" ' +
ID_ATTRIBUTE_NAME +
'="[^"]*"' +
'=""' +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
Expand All @@ -72,9 +73,10 @@ describe('ReactDOMServer', () => {
new RegExp(
'<img ' +
ROOT_ATTRIBUTE_NAME +
'="" ' +
ID_ATTRIBUTE_NAME +
'="[^"]*"' +
'=""' +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
Expand All @@ -89,9 +91,10 @@ describe('ReactDOMServer', () => {
new RegExp(
'<img data-attr="&gt;" ' +
ROOT_ATTRIBUTE_NAME +
'="" ' +
ID_ATTRIBUTE_NAME +
'="[^"]*"' +
'=""' +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
Expand Down Expand Up @@ -135,16 +138,19 @@ describe('ReactDOMServer', () => {
new RegExp(
'<div ' +
ROOT_ATTRIBUTE_NAME +
'="" ' +
ID_ATTRIBUTE_NAME +
'="[^"]*"' +
'=""' +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
'>' +
'<span ' +
ID_ATTRIBUTE_NAME +
'="[^"]*">' +
'<span' +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
'>' +
(ReactDOMFeatureFlags.useFiber
? 'My name is <!-- -->child'
: '<!-- react-text: [0-9]+ -->My name is <!-- /react-text -->' +
Expand Down Expand Up @@ -206,9 +212,10 @@ describe('ReactDOMServer', () => {
new RegExp(
'<span ' +
ROOT_ATTRIBUTE_NAME +
'="" ' +
ID_ATTRIBUTE_NAME +
'="[^"]*"' +
'=""' +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ID_ATTRIBUTE_NAME + '="[^"]*"') +
(ReactDOMFeatureFlags.useFiber
? ''
: ' ' + ReactMarkupChecksum.CHECKSUM_ATTR_NAME + '="[^"]+"') +
Expand Down
1 change: 0 additions & 1 deletion src/renderers/shared/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,6 @@ function createOpenTagMarkup(
if (isRootElement) {
ret += ' ' + DOMMarkupOperations.createMarkupForRoot();
}
ret += ' ' + DOMMarkupOperations.createMarkupForID('');
return ret;
}

Expand Down

0 comments on commit 965217d

Please sign in to comment.