Skip to content

Commit 8422a91

Browse files
committed
Add hydration API
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})
1 parent d0b1b7e commit 8422a91

File tree

4 files changed

+149
-42
lines changed

4 files changed

+149
-42
lines changed

src/renderers/dom/fiber/ReactDOMFiberEntry.js

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,20 @@ findDOMNode._injectFiber(function(fiber: Fiber) {
9393

9494
type DOMContainer =
9595
| (Element & {
96-
_reactRootContainer: ?Object,
96+
_reactRootContainer?: Object | null,
9797
})
9898
| (Document & {
99-
_reactRootContainer: ?Object,
99+
_reactRootContainer?: Object | null,
100100
});
101101

102-
type Container =
103-
| Element
104-
| Document
105-
// If the DOM container is lazily provided, the container is the namespace uri
106-
| string;
102+
type LazyContainer = {
103+
namespace: string,
104+
ownerDocument: Document,
105+
getContainer: () => Element | DOMContainer,
106+
_reactRootContainer?: Object | null,
107+
};
108+
109+
type Container = DOMContainer | LazyContainer;
107110

108111
type Props = {
109112
autoFocus?: boolean,
@@ -123,10 +126,15 @@ type HostContext = HostContextDev | HostContextProd;
123126
let eventsEnabled: ?boolean = null;
124127
let selectionInformation: ?mixed = null;
125128

129+
function isLazyContainer(container: Container): boolean {
130+
return typeof (container: any).getContainer === 'function';
131+
}
132+
126133
function getOwnerDocument(container: Container): Document {
127134
let ownerDocument;
128-
if (typeof container === 'string') {
129-
ownerDocument = document;
135+
if (isLazyContainer(container)) {
136+
const lazyContainer: LazyContainer = (container: any);
137+
ownerDocument = lazyContainer.ownerDocument;
130138
} else if (
131139
container.nodeType === DOCUMENT_NODE ||
132140
container.nodeType === DOCUMENT_FRAGMENT_NODE
@@ -138,14 +146,17 @@ function getOwnerDocument(container: Container): Document {
138146
return ownerDocument;
139147
}
140148

141-
function ensureDOMContainer(container: Container): Element | Document {
149+
function ensureDOMContainer(container: Container): DOMContainer {
150+
if (!isLazyContainer(container)) {
151+
return ((container: any): DOMContainer);
152+
}
153+
const lazyContainer: LazyContainer = (container: any);
154+
const domContainer = lazyContainer.getContainer();
142155
invariant(
143-
typeof container !== 'string',
144-
// TODO: Better error message. Probably should have errored already, when
145-
// validating the result of getContainer.
156+
container !== null && container !== undefined,
157+
// TODO: Better error message.
146158
'Container should have resolved by now',
147159
);
148-
const domContainer: Element | Document = (container: any);
149160
return domContainer;
150161
}
151162

@@ -200,8 +211,8 @@ var DOMRenderer = ReactFiberReconciler({
200211
let type;
201212
let namespace;
202213

203-
if (typeof rootContainerInstance === 'string') {
204-
namespace = rootContainerInstance;
214+
if (isLazyContainer(rootContainerInstance)) {
215+
namespace = ((rootContainerInstance: any): LazyContainer).namespace;
205216
if (__DEV__) {
206217
return {namespace, ancestorInfo: null};
207218
}
@@ -212,7 +223,7 @@ var DOMRenderer = ReactFiberReconciler({
212223
namespace = root ? root.namespaceURI : getChildNamespace(null, '');
213224
} else {
214225
const container: any = rootContainerInstance.nodeType === COMMENT_NODE
215-
? rootContainerInstance.parentNode
226+
? (rootContainerInstance: any).parentNode
216227
: rootContainerInstance;
217228
const ownNamespace = container.namespaceURI || null;
218229
type = container.tagName;
@@ -724,28 +735,21 @@ type PublicRoot = {
724735
unmount(callback: ?() => mixed): void,
725736

726737
_reactRootContainer: *,
727-
_getComponent: () => DOMContainer,
728738
};
729739

730-
function PublicRootNode(
731-
container: DOMContainer | (() => DOMContainer),
740+
type RootOptions = {
741+
hydrate?: boolean,
742+
};
743+
744+
type LazyRootOptions = {
732745
namespace?: string,
733-
) {
734-
if (typeof container === 'function') {
735-
if (typeof namespace !== 'string') {
736-
// Default to HTML namespace
737-
namespace = DOMNamespaces.html;
738-
}
739-
this._reactRootContainer = DOMRenderer.createContainer(namespace);
740-
this._getComponent = container;
741-
} else {
742-
// Assume this is a DOM container
743-
const domContainer: DOMContainer = (container: any);
744-
this._reactRootContainer = DOMRenderer.createContainer(domContainer);
745-
this._getComponent = function() {
746-
return domContainer;
747-
};
748-
}
746+
ownerDocument?: Document,
747+
};
748+
749+
function PublicRootNode(container: Container, hydrate: boolean) {
750+
const root = DOMRenderer.createContainer(container);
751+
root.hydrate = hydrate;
752+
this._reactRootContainer = root;
749753
}
750754
PublicRootNode.prototype.render = function(
751755
children: ReactNodeList,
@@ -776,10 +780,38 @@ PublicRootNode.prototype.unmount = function(callback) {
776780

777781
var ReactDOMFiber = {
778782
unstable_createRoot(
779-
container: DOMContainer | (() => DOMContainer),
780-
namespace?: string,
783+
container: DOMContainer,
784+
options?: RootOptions,
785+
): PublicRoot {
786+
let hydrate = false;
787+
if (options != null && options.hydrate !== undefined) {
788+
hydrate = options.hydrate;
789+
}
790+
return new PublicRootNode(container, hydrate);
791+
},
792+
793+
unstable_createLazyRoot(
794+
getContainer: () => DOMContainer,
795+
options?: LazyRootOptions,
781796
): PublicRoot {
782-
return new PublicRootNode(container, namespace);
797+
// Default to HTML namespace
798+
let namespace = DOMNamespaces.html;
799+
// Default to global document
800+
let ownerDocument = document;
801+
if (options != null) {
802+
if (options.namespace != null) {
803+
namespace = options.namespace;
804+
}
805+
if (options.ownerDocument != null) {
806+
ownerDocument = options.ownerDocument;
807+
}
808+
}
809+
const container = {
810+
getContainer,
811+
namespace,
812+
ownerDocument,
813+
};
814+
return new PublicRootNode(container, false);
783815
},
784816

785817
createPortal,

src/renderers/dom/shared/__tests__/ReactDOMAsyncRoot-test.js

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
1515

1616
let React;
1717
let ReactDOM;
18+
let ReactDOMServer;
1819
let ReactFeatureFlags;
1920

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

2425
React = require('react');
2526
ReactDOM = require('react-dom');
27+
ReactDOMServer = require('react-dom/server');
2628
ReactFeatureFlags = require('ReactFeatureFlags');
2729
ReactFeatureFlags.enableAsyncSubtreeAPI = true;
2830
});
@@ -48,27 +50,96 @@ describe('ReactDOMAsyncRoot', () => {
4850
// Hasn't updated yet
4951
expect(container.textContent).toEqual('');
5052

53+
let ops = [];
5154
work.then(() => {
5255
// Still hasn't updated
53-
expect(container.textContent).toEqual('');
56+
ops.push(container.textContent);
5457
// Should synchronously commit
5558
work.commit();
56-
expect(container.textContent).toEqual('Foo');
59+
ops.push(container.textContent);
5760
});
58-
61+
// Flush async work
5962
jest.runAllTimers();
63+
expect(ops).toEqual(['', 'Foo']);
6064
});
6165

6266
it('resolves `then` callback synchronously if update is sync', () => {
6367
const container = document.createElement('div');
6468
const root = ReactDOM.unstable_createRoot(container);
6569
const work = root.prerender(<div>Hi</div>);
70+
71+
let ops = [];
6672
work.then(() => {
6773
work.commit();
74+
ops.push(container.textContent);
6875
expect(container.textContent).toEqual('Hi');
6976
});
7077
// `then` should have synchronously resolved
78+
expect(ops).toEqual(['Hi']);
79+
});
80+
81+
it('supports hydration', async () => {
82+
const markup = await new Promise(resolve =>
83+
resolve(
84+
ReactDOMServer.renderToString(<div><span className="extra" /></div>),
85+
),
86+
);
87+
88+
spyOn(console, 'error');
89+
90+
// Does not hydrate by default
91+
const container1 = document.createElement('div');
92+
container1.innerHTML = markup;
93+
const root1 = ReactDOM.unstable_createRoot(container1);
94+
root1.render(<div><span /></div>);
95+
expect(console.error.calls.count()).toBe(0);
96+
97+
// Accepts `hydrate` option
98+
const container2 = document.createElement('div');
99+
container2.innerHTML = markup;
100+
const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true});
101+
root2.render(<div><span /></div>);
102+
expect(console.error.calls.count()).toBe(1);
103+
expect(console.error.calls.argsFor(0)[0]).toMatch('Extra attributes');
104+
});
105+
106+
it('supports lazy containers', () => {
107+
let ops = [];
108+
function Foo(props) {
109+
ops.push('Foo');
110+
return props.children;
111+
}
112+
113+
let container;
114+
const root = ReactDOM.unstable_createLazyRoot(() => container);
115+
const work = root.prerender(<Foo>Hi</Foo>);
116+
expect(ops).toEqual(['Foo']);
117+
118+
// Set container
119+
container = document.createElement('div');
120+
121+
ops = [];
122+
123+
work.commit();
71124
expect(container.textContent).toEqual('Hi');
125+
// Should not have re-rendered Foo
126+
expect(ops).toEqual([]);
127+
});
128+
129+
it('can specify namespace of a lazy container', () => {
130+
const namespace = 'http://www.w3.org/2000/svg';
131+
132+
let container;
133+
const root = ReactDOM.unstable_createLazyRoot(() => container, {
134+
namespace,
135+
});
136+
const work = root.prerender(<path />);
137+
138+
// Set container
139+
container = document.createElementNS(namespace, 'svg');
140+
work.commit();
141+
// Child should have svg namespace
142+
expect(container.firstChild.namespaceURI).toBe(namespace);
72143
});
73144
} else {
74145
it('does not apply to stack');

src/renderers/shared/fiber/ReactFiberBeginWork.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
364364
}
365365
const element = state.element;
366366
if (
367+
root.hydrate &&
367368
(current === null || current.child === null) &&
368369
enterHydrationState(workInProgress)
369370
) {

src/renderers/shared/fiber/ReactFiberRoot.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export type FiberRoot = {
4444
// Top context object, used by renderSubtreeIntoContainer
4545
context: Object | null,
4646
pendingContext: Object | null,
47+
// Determines if we should attempt to hydrate on the initial mount
48+
hydrate: boolean,
4749
};
4850

4951
exports.isRootBlocked = function(
@@ -73,6 +75,7 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot {
7375
nextScheduledRoot: null,
7476
context: null,
7577
pendingContext: null,
78+
hydrate: true,
7679
};
7780
uninitializedFiber.stateNode = root;
7881
return root;

0 commit comments

Comments
 (0)