Skip to content

Commit

Permalink
Add hydration API
Browse files Browse the repository at this point in the history
Hydration should be disabled by default. It's also incompatible with
lazy containers, since you can only hydrate a container that has
already resolved.

After considering these constraints, we came up with this API:

createRoot(container: Element, ?{hydrate?: boolean})
createLazyRoot(container: () => Element, ?{namespace?: string, ownerDocument?: Document})
  • Loading branch information
acdlite committed Sep 29, 2017
1 parent 6cee370 commit f59a531
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 42 deletions.
110 changes: 71 additions & 39 deletions src/renderers/dom/fiber/ReactDOMFiberEntry.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,20 @@ findDOMNode._injectFiber(function(fiber: Fiber) {

type DOMContainer =
| (Element & {
_reactRootContainer: ?Object,
_reactRootContainer?: Object | null,
})
| (Document & {
_reactRootContainer: ?Object,
_reactRootContainer?: Object | null,
});

type Container =
| Element
| Document
// If the DOM container is lazily provided, the container is the namespace uri
| string;
type LazyContainer = {
namespace: string,
ownerDocument: Document,
getContainer: () => Element | DOMContainer,
_reactRootContainer?: Object | null,
};

type Container = DOMContainer | LazyContainer;

type Props = {
autoFocus?: boolean,
Expand All @@ -122,10 +125,15 @@ type HostContext = HostContextDev | HostContextProd;
let eventsEnabled: ?boolean = null;
let selectionInformation: ?mixed = null;

function isLazyContainer(container: Container): boolean {
return typeof (container: any).getContainer === 'function';
}

function getOwnerDocument(container: Container): Document {
let ownerDocument;
if (typeof container === 'string') {
ownerDocument = document;
if (isLazyContainer(container)) {
const lazyContainer: LazyContainer = (container: any);
ownerDocument = lazyContainer.ownerDocument;
} else if (
container.nodeType === DOCUMENT_NODE ||
container.nodeType === DOCUMENT_FRAGMENT_NODE
Expand All @@ -137,14 +145,17 @@ function getOwnerDocument(container: Container): Document {
return ownerDocument;
}

function ensureDOMContainer(container: Container): Element | Document {
function ensureDOMContainer(container: Container): DOMContainer {
if (!isLazyContainer(container)) {
return ((container: any): DOMContainer);
}
const lazyContainer: LazyContainer = (container: any);
const domContainer = lazyContainer.getContainer();
invariant(
typeof container !== 'string',
// TODO: Better error message. Probably should have errored already, when
// validating the result of getContainer.
container !== null && container !== undefined,
// TODO: Better error message.
'Container should have resolved by now',
);
const domContainer: Element | Document = (container: any);
return domContainer;
}

Expand Down Expand Up @@ -199,8 +210,8 @@ var DOMRenderer = ReactFiberReconciler({
let type;
let namespace;

if (typeof rootContainerInstance === 'string') {
namespace = rootContainerInstance;
if (isLazyContainer(rootContainerInstance)) {
namespace = ((rootContainerInstance: any): LazyContainer).namespace;
if (__DEV__) {
return {namespace, ancestorInfo: null};
}
Expand All @@ -211,7 +222,7 @@ var DOMRenderer = ReactFiberReconciler({
namespace = root ? root.namespaceURI : getChildNamespace(null, '');
} else {
const container: any = rootContainerInstance.nodeType === COMMENT_NODE
? rootContainerInstance.parentNode
? (rootContainerInstance: any).parentNode
: rootContainerInstance;
const ownNamespace = container.namespaceURI || null;
type = container.tagName;
Expand Down Expand Up @@ -724,28 +735,21 @@ type PublicRoot = {
unmount(callback: ?() => mixed): void,

_reactRootContainer: *,
_getComponent: () => DOMContainer,
};

function PublicRootNode(
container: DOMContainer | (() => DOMContainer),
type RootOptions = {
hydrate?: boolean,
};

type LazyRootOptions = {
namespace?: string,
) {
if (typeof container === 'function') {
if (typeof namespace !== 'string') {
// Default to HTML namespace
namespace = DOMNamespaces.html;
}
this._reactRootContainer = DOMRenderer.createContainer(namespace);
this._getComponent = container;
} else {
// Assume this is a DOM container
const domContainer: DOMContainer = (container: any);
this._reactRootContainer = DOMRenderer.createContainer(domContainer);
this._getComponent = function() {
return domContainer;
};
}
ownerDocument?: Document,
};

function PublicRootNode(container: Container, hydrate: boolean) {
const root = DOMRenderer.createContainer(container);
root.hydrate = hydrate;
this._reactRootContainer = root;
}
PublicRootNode.prototype.render = function(
children: ReactNodeList,
Expand Down Expand Up @@ -776,10 +780,38 @@ PublicRootNode.prototype.unmount = function(callback) {

var ReactDOMFiber = {
unstable_createRoot(
container: DOMContainer | (() => DOMContainer),
namespace?: string,
container: DOMContainer,
options?: RootOptions,
): PublicRoot {
let hydrate = false;
if (options != null && options.hydrate !== undefined) {
hydrate = options.hydrate;
}
return new PublicRootNode(container, hydrate);
},

unstable_createLazyRoot(
getContainer: () => DOMContainer,
options?: LazyRootOptions,
): PublicRoot {
return new PublicRootNode(container, namespace);
// Default to HTML namespace
let namespace = DOMNamespaces.html;
// Default to global document
let ownerDocument = document;
if (options != null) {
if (options.namespace != null) {
namespace = options.namespace;
}
if (options.ownerDocument != null) {
ownerDocument = options.ownerDocument;
}
}
const container = {
getContainer,
namespace,
ownerDocument,
};
return new PublicRootNode(container, false);
},

createPortal,
Expand Down
77 changes: 74 additions & 3 deletions src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');

let React;
let ReactDOM;
let ReactDOMServer;
let ReactFeatureFlags;

describe('ReactDOMAsyncRoot', () => {
Expand All @@ -23,6 +24,7 @@ describe('ReactDOMAsyncRoot', () => {

React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
ReactFeatureFlags = require('ReactFeatureFlags');
ReactFeatureFlags.enableAsyncSubtreeAPI = true;
});
Expand All @@ -48,27 +50,96 @@ describe('ReactDOMAsyncRoot', () => {
// Hasn't updated yet
expect(container.textContent).toEqual('');

let ops = [];
work.then(() => {
// Still hasn't updated
expect(container.textContent).toEqual('');
ops.push(container.textContent);
// Should synchronously commit
work.commit();
expect(container.textContent).toEqual('Foo');
ops.push(container.textContent);
});

// Flush async work
jest.runAllTimers();
expect(ops).toEqual(['', 'Foo']);
});

it('resolves `then` callback synchronously if update is sync', () => {
const container = document.createElement('div');
const root = ReactDOM.unstable_createRoot(container);
const work = root.prerender(<div>Hi</div>);

let ops = [];
work.then(() => {
work.commit();
ops.push(container.textContent);
expect(container.textContent).toEqual('Hi');
});
// `then` should have synchronously resolved
expect(ops).toEqual(['Hi']);
});

it('supports hydration', async () => {
const markup = await new Promise(resolve =>
resolve(
ReactDOMServer.renderToString(<div><span className="extra" /></div>),
),
);

spyOn(console, 'error');

// Does not hydrate by default
const container1 = document.createElement('div');
container1.innerHTML = markup;
const root1 = ReactDOM.unstable_createRoot(container1);
root1.render(<div><span /></div>);
expect(console.error.calls.count()).toBe(0);

// Accepts `hydrate` option
const container2 = document.createElement('div');
container2.innerHTML = markup;
const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true});
root2.render(<div><span /></div>);
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toMatch('Extra attributes');
});

it('supports lazy containers', () => {
let ops = [];
function Foo(props) {
ops.push('Foo');
return props.children;
}

let container;
const root = ReactDOM.unstable_createLazyRoot(() => container);
const work = root.prerender(<Foo>Hi</Foo>);
expect(ops).toEqual(['Foo']);

// Set container
container = document.createElement('div');

ops = [];

work.commit();
expect(container.textContent).toEqual('Hi');
// Should not have re-rendered Foo
expect(ops).toEqual([]);
});

it('can specify namespace of a lazy container', () => {
const namespace = 'http://www.w3.org/2000/svg';

let container;
const root = ReactDOM.unstable_createLazyRoot(() => container, {
namespace,
});
const work = root.prerender(<path />);

// Set container
container = document.createElementNS(namespace, 'svg');
work.commit();
// Child should have svg namespace
expect(container.firstChild.namespaceURI).toBe(namespace);
});
} else {
it('does not apply to stack');
Expand Down
1 change: 1 addition & 0 deletions src/renderers/shared/fiber/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
}
const element = state.element;
if (
root.hydrate &&
(current === null || current.child === null) &&
enterHydrationState(workInProgress)
) {
Expand Down
3 changes: 3 additions & 0 deletions src/renderers/shared/fiber/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ 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.isRootBlocked = function(
Expand Down Expand Up @@ -71,6 +73,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot {
nextScheduledRoot: null,
context: null,
pendingContext: null,
hydrate: true,
};
uninitializedFiber.stateNode = root;
return root;
Expand Down

0 comments on commit f59a531

Please sign in to comment.