@samitouri / QOS-React / commits / 1b55ad2a4b

root.createBatch (#11473)

API for batching top-level updates and deferring the commit. - `root.createBatch` creates a batch with an async expiration time associated with it. - `batch.render` updates the children that the batch renders. - `batch.then` resolves when the root has completed. - `batch.commit` synchronously flushes any remaining work and commits. No two batches can have the same expiration time. The only way to commit a batch is by calling its `commit` method. E.g. flushing one batch will not cause a different batch to also flush.

Andrew Clark committed Nov 28, 2017 at 16:48 UTC 1b55ad2a4b24b06ce34f77a943396228641d74a1
6 files changed +870 -169
packages/react-dom/src/__tests__/ReactDOMRoot-test.internal.js
+230 -1
@@ -9,16 +9,68 @@
9
10 'use strict';
11
12 -require('shared/ReactFeatureFlags').enableCreateRoot = true;
12 var React = require('react');
13 var ReactDOM = require('react-dom');
14 var ReactDOMServer = require('react-dom/server');
15 +var AsyncComponent = React.unstable_AsyncComponent;
16
17 describe('ReactDOMRoot', () => {
18 let container;
19
20 + let scheduledCallback;
21 + let flush;
22 + let now;
23 + let expire;
24 +
25 beforeEach(() => {
26 container = document.createElement('div');
27 +
28 + // Override requestIdleCallback
29 + scheduledCallback = null;
30 + flush = function(units = Infinity) {
31 + if (scheduledCallback !== null) {
32 + var didStop = false;
33 + while (scheduledCallback !== null && !didStop) {
34 + const cb = scheduledCallback;
35 + scheduledCallback = null;
36 + cb({
37 + timeRemaining() {
38 + if (units > 0) {
39 + return 999;
40 + }
41 + didStop = true;
42 + return 0;
43 + },
44 + });
45 + units--;
46 + }
47 + }
48 + };
49 + global.performance = {
50 + now() {
51 + return now;
52 + },
53 + };
54 + global.requestIdleCallback = function(cb) {
55 + scheduledCallback = cb;
56 + };
57 +
58 + now = 0;
59 + expire = function(ms) {
60 + now += ms;
61 + };
62 + global.performance = {
63 + now() {
64 + return now;
65 + },
66 + };
67 +
68 + jest.resetModules();
69 + require('shared/ReactFeatureFlags').enableCreateRoot = true;
70 + React = require('react');
71 + ReactDOM = require('react-dom');
72 + ReactDOMServer = require('react-dom/server');
73 + AsyncComponent = React.unstable_AsyncComponent;
74 });
75
76 it('renders children', () => {
@@ -35,6 +87,35 @@ describe('ReactDOMRoot', () => {
87 expect(container.textContent).toEqual('');
88 });
89
90 + it('`root.render` returns a thenable work object', () => {
91 + const root = ReactDOM.createRoot(container);
92 + const work = root.render(<AsyncComponent>Hi</AsyncComponent>);
93 + let ops = [];
94 + work.then(() => {
95 + ops.push('inside callback: ' + container.textContent);
96 + });
97 + ops.push('before committing: ' + container.textContent);
98 + flush();
99 + ops.push('after committing: ' + container.textContent);
100 + expect(ops).toEqual([
101 + 'before committing: ',
102 + // `then` callback should fire during commit phase
103 + 'inside callback: Hi',
104 + 'after committing: Hi',
105 + ]);
106 + });
107 +
108 + it('resolves `work.then` callback synchronously if the work already committed', () => {
109 + const root = ReactDOM.createRoot(container);
110 + const work = root.render(<AsyncComponent>Hi</AsyncComponent>);
111 + flush();
112 + let ops = [];
113 + work.then(() => {
114 + ops.push('inside callback');
115 + });
116 + expect(ops).toEqual(['inside callback']);
117 + });
118 +
119 it('supports hydration', async () => {
120 const markup = await new Promise(resolve =>
121 resolve(
@@ -95,4 +176,152 @@ describe('ReactDOMRoot', () => {
176 );
177 expect(container.textContent).toEqual('abdc');
178 });
179 +
180 + it('can defer a commit by batching it', () => {
181 + const root = ReactDOM.createRoot(container);
182 + const batch = root.createBatch();
183 + batch.render(<div>Hi</div>);
184 + // Hasn't committed yet
185 + expect(container.textContent).toEqual('');
186 + // Commit
187 + batch.commit();
188 + expect(container.textContent).toEqual('Hi');
189 + });
190 +
191 + it('does not restart a completed batch when committing if there were no intervening updates', () => {
192 + let ops = [];
193 + function Foo(props) {
194 + ops.push('Foo');
195 + return props.children;
196 + }
197 + const root = ReactDOM.createRoot(container);
198 + const batch = root.createBatch();
199 + batch.render(<Foo>Hi</Foo>);
200 + // Flush all async work.
201 + flush();
202 + // Root should complete without committing.
203 + expect(ops).toEqual(['Foo']);
204 + expect(container.textContent).toEqual('');
205 +
206 + ops = [];
207 +
208 + // Commit. Shouldn't re-render Foo.
209 + batch.commit();
210 + expect(ops).toEqual([]);
211 + expect(container.textContent).toEqual('Hi');
212 + });
213 +
214 + it('can wait for a batch to finish', () => {
215 + const Async = React.unstable_AsyncComponent;
216 + const root = ReactDOM.createRoot(container);
217 + const batch = root.createBatch();
218 + batch.render(<Async>Foo</Async>);
219 +
220 + flush();
221 +
222 + // Hasn't updated yet
223 + expect(container.textContent).toEqual('');
224 +
225 + let ops = [];
226 + batch.then(() => {
227 + // Still hasn't updated
228 + ops.push(container.textContent);
229 + // Should synchronously commit
230 + batch.commit();
231 + ops.push(container.textContent);
232 + });
233 +
234 + expect(ops).toEqual(['', 'Foo']);
235 + });
236 +
237 + it('`batch.render` returns a thenable work object', () => {
238 + const root = ReactDOM.createRoot(container);
239 + const batch = root.createBatch();
240 + const work = batch.render('Hi');
241 + let ops = [];
242 + work.then(() => {
243 + ops.push('inside callback: ' + container.textContent);
244 + });
245 + ops.push('before committing: ' + container.textContent);
246 + batch.commit();
247 + ops.push('after committing: ' + container.textContent);
248 + expect(ops).toEqual([
249 + 'before committing: ',
250 + // `then` callback should fire during commit phase
251 + 'inside callback: Hi',
252 + 'after committing: Hi',
253 + ]);
254 + });
255 +
256 + it('can commit an empty batch', () => {
257 + const root = ReactDOM.createRoot(container);
258 + root.render(<AsyncComponent>1</AsyncComponent>);
259 +
260 + expire(2000);
261 + // This batch has a later expiration time than the earlier update.
262 + const batch = root.createBatch();
263 +
264 + // This should not flush the earlier update.
265 + batch.commit();
266 + expect(container.textContent).toEqual('');
267 +
268 + flush();
269 + expect(container.textContent).toEqual('1');
270 + });
271 +
272 + it('two batches created simultaneously are committed separately', () => {
273 + // (In other words, they have distinct expiration times)
274 + const root = ReactDOM.createRoot(container);
275 + const batch1 = root.createBatch();
276 + batch1.render(1);
277 + const batch2 = root.createBatch();
278 + batch2.render(2);
279 +
280 + expect(container.textContent).toEqual('');
281 +
282 + batch1.commit();
283 + expect(container.textContent).toEqual('1');
284 +
285 + batch2.commit();
286 + expect(container.textContent).toEqual('2');
287 + });
288 +
289 + it('commits an earlier batch without committing a later batch', () => {
290 + const root = ReactDOM.createRoot(container);
291 + const batch1 = root.createBatch();
292 + batch1.render(1);
293 +
294 + // This batch has a later expiration time
295 + expire(2000);
296 + const batch2 = root.createBatch();
297 + batch2.render(2);
298 +
299 + expect(container.textContent).toEqual('');
300 +
301 + batch1.commit();
302 + expect(container.textContent).toEqual('1');
303 +
304 + batch2.commit();
305 + expect(container.textContent).toEqual('2');
306 + });
307 +
308 + it('commits a later batch without commiting an earlier batch', () => {
309 + const root = ReactDOM.createRoot(container);
310 + const batch1 = root.createBatch();
311 + batch1.render(1);
312 +
313 + // This batch has a later expiration time
314 + expire(2000);
315 + const batch2 = root.createBatch();
316 + batch2.render(2);
317 +
318 + expect(container.textContent).toEqual('');
319 +
320 + batch2.commit();
321 + expect(container.textContent).toEqual('2');
322 +
323 + batch1.commit();
324 + flush();
325 + expect(container.textContent).toEqual('1');
326 + });
327 });
packages/react-dom/src/client/ReactDOM.js
+443 -115
@@ -8,6 +8,12 @@
8 */
9
10 import type {ReactNodeList} from 'shared/ReactTypes';
11 +// TODO: This type is shared between the reconciler and ReactDOM, but will
12 +// eventually be lifted out to the renderer.
13 +import type {
14 + FiberRoot,
15 + Batch as FiberRootBatch,
16 +} from 'react-reconciler/src/ReactFiberRoot';
17
18 import '../shared/checkReact';
19 import '../shared/ReactDOMInjection';
@@ -84,6 +90,63 @@ if (__DEV__) {
90 'polyfill in older browsers. http://fb.me/react-polyfills',
91 );
92 }
93 +
94 + var topLevelUpdateWarnings = (container: DOMContainer) => {
95 + if (__DEV__) {
96 + if (
97 + container._reactRootContainer &&
98 + container.nodeType !== COMMENT_NODE
99 + ) {
100 + const hostInstance = DOMRenderer.findHostInstanceWithNoPortals(
101 + container._reactRootContainer._internalRoot.current,
102 + );
103 + if (hostInstance) {
104 + warning(
105 + hostInstance.parentNode === container,
106 + 'render(...): It looks like the React-rendered content of this ' +
107 + 'container was removed without using React. This is not ' +
108 + 'supported and will cause errors. Instead, call ' +
109 + 'ReactDOM.unmountComponentAtNode to empty a container.',
110 + );
111 + }
112 + }
113 +
114 + const isRootRenderedBySomeReact = !!container._reactRootContainer;
115 + const rootEl = getReactRootElementInContainer(container);
116 + const hasNonRootReactChild = !!(
117 + rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl)
118 + );
119 +
120 + warning(
121 + !hasNonRootReactChild || isRootRenderedBySomeReact,
122 + 'render(...): Replacing React-rendered children with a new root ' +
123 + 'component. If you intended to update the children of this node, ' +
124 + 'you should instead have the existing children update their state ' +
125 + 'and render the new components instead of calling ReactDOM.render.',
126 + );
127 +
128 + warning(
129 + container.nodeType !== ELEMENT_NODE ||
130 + !((container: any): Element).tagName ||
131 + ((container: any): Element).tagName.toUpperCase() !== 'BODY',
132 + 'render(): Rendering components directly into document.body is ' +
133 + 'discouraged, since its children are often manipulated by third-party ' +
134 + 'scripts and browser extensions. This may lead to subtle ' +
135 + 'reconciliation issues. Try rendering into a container element created ' +
136 + 'for your app.',
137 + );
138 + }
139 + };
140 +
141 + var warnOnInvalidCallback = function(callback: mixed, callerName: string) {
142 + warning(
143 + callback === null || typeof callback === 'function',
144 + '%s(...): Expected the last optional `callback` argument to be a ' +
145 + 'function. Instead received: %s.',
146 + callerName,
147 + callback,
148 + );
149 + };
150 }
151
152 ReactControlledComponent.injection.injectFiberControlledHostComponent(
@@ -92,10 +155,10 @@ ReactControlledComponent.injection.injectFiberControlledHostComponent(
155
156 type DOMContainer =
157 | (Element & {
95 - _reactRootContainer: ?Object,
158 + _reactRootContainer: ?Root,
159 })
160 | (Document & {
98 - _reactRootContainer: ?Object,
161 + _reactRootContainer: ?Root,
162 });
163
164 type Container = Element | Document;
@@ -118,6 +181,281 @@ type HostContext = HostContextDev | HostContextProd;
181 let eventsEnabled: ?boolean = null;
182 let selectionInformation: ?mixed = null;
183
184 +type Batch = FiberRootBatch & {
185 + render(children: ReactNodeList): Work,
186 + then(onComplete: () => mixed): void,
187 + commit(): void,
188 +
189 + // The ReactRoot constuctor is hoisted but the prototype methods are not. If
190 + // we move ReactRoot to be above ReactBatch, the inverse error occurs.
191 + // $FlowFixMe Hoisting issue.
192 + _root: Root,
193 + _hasChildren: boolean,
194 + _children: ReactNodeList,
195 +
196 + _callbacks: Array<() => mixed> | null,
197 + _didComplete: boolean,
198 +};
199 +
200 +function ReactBatch(root: ReactRoot) {
201 + const expirationTime = DOMRenderer.computeUniqueAsyncExpiration();
202 + this._expirationTime = expirationTime;
203 + this._root = root;
204 + this._next = null;
205 + this._callbacks = null;
206 + this._didComplete = false;
207 + this._hasChildren = false;
208 + this._children = null;
209 + this._defer = true;
210 +}
211 +ReactBatch.prototype.render = function(children: ReactNodeList) {
212 + invariant(
213 + this._defer,
214 + 'batch.render: Cannot render a batch that already committed.',
215 + );
216 + this._hasChildren = true;
217 + this._children = children;
218 + const internalRoot = this._root._internalRoot;
219 + const expirationTime = this._expirationTime;
220 + const work = new ReactWork();
221 + DOMRenderer.updateContainerAtExpirationTime(
222 + children,
223 + internalRoot,
224 + null,
225 + expirationTime,
226 + work._onCommit,
227 + );
228 + return work;
229 +};
230 +ReactBatch.prototype.then = function(onComplete: () => mixed) {
231 + if (this._didComplete) {
232 + onComplete();
233 + return;
234 + }
235 + let callbacks = this._callbacks;
236 + if (callbacks === null) {
237 + callbacks = this._callbacks = [];
238 + }
239 + callbacks.push(onComplete);
240 +};
241 +ReactBatch.prototype.commit = function() {
242 + const internalRoot = this._root._internalRoot;
243 + let firstBatch = internalRoot.firstBatch;
244 + invariant(
245 + this._defer && firstBatch !== null,
246 + 'batch.commit: Cannot commit a batch multiple times.',
247 + );
248 +
249 + if (!this._hasChildren) {
250 + // This batch is empty. Return.
251 + this._next = null;
252 + this._defer = false;
253 + return;
254 + }
255 +
256 + let expirationTime = this._expirationTime;
257 +
258 + // Ensure this is the first batch in the list.
259 + if (firstBatch !== this) {
260 + // This batch is not the earliest batch. We need to move it to the front.
261 + // Update its expiration time to be the expiration time of the earliest
262 + // batch, so that we can flush it without flushing the other batches.
263 + if (this._hasChildren) {
264 + expirationTime = this._expirationTime = firstBatch._expirationTime;
265 + // Rendering this batch again ensures its children will be the final state
266 + // when we flush (updates are processed in insertion order: last
267 + // update wins).
268 + // TODO: This forces a restart. Should we print a warning?
269 + this.render(this._children);
270 + }
271 +
272 + // Remove the batch from the list.
273 + let previous = null;
274 + let batch = firstBatch;
275 + while (batch !== this) {
276 + previous = batch;
277 + batch = batch._next;
278 + }
279 + invariant(
280 + previous !== null,
281 + 'batch.commit: Cannot commit a batch multiple times.',
282 + );
283 + previous._next = batch._next;
284 +
285 + // Add it to the front.
286 + this._next = firstBatch;
287 + firstBatch = internalRoot.firstBatch = this;
288 + }
289 +
290 + // Synchronously flush all the work up to this batch's expiration time.
291 + this._defer = false;
292 + DOMRenderer.flushRoot(internalRoot, expirationTime);
293 +
294 + // Pop the batch from the list.
295 + const next = this._next;
296 + this._next = null;
297 + firstBatch = internalRoot.firstBatch = next;
298 +
299 + // Append the next earliest batch's children to the update queue.
300 + if (firstBatch !== null && firstBatch._hasChildren) {
301 + firstBatch.render(firstBatch._children);
302 + }
303 +};
304 +ReactBatch.prototype._onComplete = function() {
305 + if (this._didComplete) {
306 + return;
307 + }
308 + this._didComplete = true;
309 + const callbacks = this._callbacks;
310 + if (callbacks === null) {
311 + return;
312 + }
313 + // TODO: Error handling.
314 + for (let i = 0; i < callbacks.length; i++) {
315 + const callback = callbacks[i];
316 + callback();
317 + }
318 +};
319 +
320 +type Work = {
321 + then(onCommit: () => mixed): void,
322 + _onCommit: () => void,
323 + _callbacks: Array<() => mixed> | null,
324 + _didCommit: boolean,
325 +};
326 +
327 +function ReactWork() {
328 + this._callbacks = null;
329 + this._didCommit = false;
330 + // TODO: Avoid need to bind by replacing callbacks in the update queue with
331 + // list of Work objects.
332 + this._onCommit = this._onCommit.bind(this);
333 +}
334 +ReactWork.prototype.then = function(onCommit: () => mixed): void {
335 + if (this._didCommit) {
336 + onCommit();
337 + return;
338 + }
339 + let callbacks = this._callbacks;
340 + if (callbacks === null) {
341 + callbacks = this._callbacks = [];
342 + }
343 + callbacks.push(onCommit);
344 +};
345 +ReactWork.prototype._onCommit = function(): void {
346 + if (this._didCommit) {
347 + return;
348 + }
349 + this._didCommit = true;
350 + const callbacks = this._callbacks;
351 + if (callbacks === null) {
352 + return;
353 + }
354 + // TODO: Error handling.
355 + for (let i = 0; i < callbacks.length; i++) {
356 + const callback = callbacks[i];
357 + invariant(
358 + typeof callback === 'function',
359 + 'Invalid argument passed as callback. Expected a function. Instead ' +
360 + 'received: %s',
361 + callback,
362 + );
363 + callback();
364 + }
365 +};
366 +
367 +type Root = {
368 + render(children: ReactNodeList, callback: ?() => mixed): Work,
369 + unmount(callback: ?() => mixed): Work,
370 + legacy_renderSubtreeIntoContainer(
371 + parentComponent: ?React$Component<any, any>,
372 + children: ReactNodeList,
373 + callback: ?() => mixed,
374 + ): Work,
375 + createBatch(): Batch,
376 +
377 + _internalRoot: FiberRoot,
378 +};
379 +
380 +function ReactRoot(container: Container, hydrate: boolean) {
381 + const root = DOMRenderer.createContainer(container, hydrate);
382 + this._internalRoot = root;
383 +}
384 +ReactRoot.prototype.render = function(
385 + children: ReactNodeList,
386 + callback: ?() => mixed,
387 +): Work {
388 + const root = this._internalRoot;
389 + const work = new ReactWork();
390 + callback = callback === undefined ? null : callback;
391 + if (__DEV__) {
392 + warnOnInvalidCallback(callback, 'render');
393 + }
394 + if (callback !== null) {
395 + work.then(callback);
396 + }
397 + DOMRenderer.updateContainer(children, root, null, work._onCommit);
398 + return work;
399 +};
400 +ReactRoot.prototype.unmount = function(callback: ?() => mixed): Work {
401 + const root = this._internalRoot;
402 + const work = new ReactWork();
403 + callback = callback === undefined ? null : callback;
404 + if (__DEV__) {
405 + warnOnInvalidCallback(callback, 'render');
406 + }
407 + if (callback !== null) {
408 + work.then(callback);
409 + }
410 + DOMRenderer.updateContainer(null, root, null, work._onCommit);
411 + return work;
412 +};
413 +ReactRoot.prototype.legacy_renderSubtreeIntoContainer = function(
414 + parentComponent: ?React$Component<any, any>,
415 + children: ReactNodeList,
416 + callback: ?() => mixed,
417 +): Work {
418 + const root = this._internalRoot;
419 + const work = new ReactWork();
420 + callback = callback === undefined ? null : callback;
421 + if (__DEV__) {
422 + warnOnInvalidCallback(callback, 'render');
423 + }
424 + if (callback !== null) {
425 + work.then(callback);
426 + }
427 + DOMRenderer.updateContainer(children, root, parentComponent, work._onCommit);
428 + return work;
429 +};
430 +ReactRoot.prototype.createBatch = function(): Batch {
431 + const batch = new ReactBatch(this);
432 + const expirationTime = batch._expirationTime;
433 +
434 + const internalRoot = this._internalRoot;
435 + const firstBatch = internalRoot.firstBatch;
436 + if (firstBatch === null) {
437 + internalRoot.firstBatch = batch;
438 + batch._next = null;
439 + } else {
440 + // Insert sorted by expiration time then insertion order
441 + let insertAfter = null;
442 + let insertBefore = firstBatch;
443 + while (
444 + insertBefore !== null &&
445 + insertBefore._expirationTime <= expirationTime
446 + ) {
447 + insertAfter = insertBefore;
448 + insertBefore = insertBefore._next;
449 + }
450 + batch._next = insertBefore;
451 + if (insertAfter !== null) {
452 + insertAfter._next = batch;
453 + }
454 + }
455 +
456 + return batch;
457 +};
458 +
459 /**
460 * True if the supplied DOM node is a valid node element.
461 *
@@ -654,108 +992,115 @@ ReactGenericBatching.injection.injectFiberBatchedUpdates(
992
993 let warnedAboutHydrateAPI = false;
994
657 -function renderSubtreeIntoContainer(
995 +function legacyCreateRootFromDOMContainer(
996 + container: DOMContainer,
997 + forceHydrate: boolean,
998 +): Root {
999 + const shouldHydrate =
1000 + forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
1001 + // First clear any existing content.
1002 + if (!shouldHydrate) {
1003 + let warned = false;
1004 + let rootSibling;
1005 + while ((rootSibling = container.lastChild)) {
1006 + if (__DEV__) {
1007 + if (
1008 + !warned &&
1009 + rootSibling.nodeType === ELEMENT_NODE &&
1010 + (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
1011 + ) {
1012 + warned = true;
1013 + warning(
1014 + false,
1015 + 'render(): Target node has markup rendered by React, but there ' +
1016 + 'are unrelated nodes as well. This is most commonly caused by ' +
1017 + 'white-space inserted around server-rendered markup.',
1018 + );
1019 + }
1020 + }
1021 + container.removeChild(rootSibling);
1022 + }
1023 + }
1024 + if (__DEV__) {
1025 + if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
1026 + warnedAboutHydrateAPI = true;
1027 + lowPriorityWarning(
1028 + false,
1029 + 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
1030 + 'will stop working in React v17. Replace the ReactDOM.render() call ' +
1031 + 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
1032 + );
1033 + }
1034 + }
1035 + const root: Root = new ReactRoot(container, shouldHydrate);
1036 + return root;
1037 +}
1038 +
1039 +function legacyRenderSubtreeIntoContainer(
1040 parentComponent: ?React$Component<any, any>,
1041 children: ReactNodeList,
1042 container: DOMContainer,
1043 forceHydrate: boolean,
1044 callback: ?Function,
1045 ) {
1046 + // TODO: Ensure all entry points contain this check
1047 invariant(
1048 isValidContainer(container),
1049 'Target container is not a DOM element.',
1050 );
1051
1052 if (__DEV__) {
670 - if (container._reactRootContainer && container.nodeType !== COMMENT_NODE) {
671 - const hostInstance = DOMRenderer.findHostInstanceWithNoPortals(
672 - container._reactRootContainer.current,
673 - );
674 - if (hostInstance) {
675 - warning(
676 - hostInstance.parentNode === container,
677 - 'render(...): It looks like the React-rendered content of this ' +
678 - 'container was removed without using React. This is not ' +
679 - 'supported and will cause errors. Instead, call ' +
680 - 'ReactDOM.unmountComponentAtNode to empty a container.',
681 - );
682 - }
683 - }
684 -
685 - const isRootRenderedBySomeReact = !!container._reactRootContainer;
686 - const rootEl = getReactRootElementInContainer(container);
687 - const hasNonRootReactChild = !!(
688 - rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl)
689 - );
690 -
691 - warning(
692 - !hasNonRootReactChild || isRootRenderedBySomeReact,
693 - 'render(...): Replacing React-rendered children with a new root ' +
694 - 'component. If you intended to update the children of this node, ' +
695 - 'you should instead have the existing children update their state ' +
696 - 'and render the new components instead of calling ReactDOM.render.',
697 - );
698 -
699 - warning(
700 - container.nodeType !== ELEMENT_NODE ||
701 - !((container: any): Element).tagName ||
702 - ((container: any): Element).tagName.toUpperCase() !== 'BODY',
703 - 'render(): Rendering components directly into document.body is ' +
704 - 'discouraged, since its children are often manipulated by third-party ' +
705 - 'scripts and browser extensions. This may lead to subtle ' +
706 - 'reconciliation issues. Try rendering into a container element created ' +
707 - 'for your app.',
708 - );
1053 + topLevelUpdateWarnings(container);
1054 }
1055
711 - let root = container._reactRootContainer;
1056 + // TODO: Without `any` type, Flow says "Property cannot be accessed on any
1057 + // member of intersection type." Whyyyyyy.
1058 + let root: Root = (container._reactRootContainer: any);
1059 if (!root) {
713 - const shouldHydrate =
714 - forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
715 - // First clear any existing content.
716 - if (!shouldHydrate) {
717 - let warned = false;
718 - let rootSibling;
719 - while ((rootSibling = container.lastChild)) {
720 - if (__DEV__) {
721 - if (
722 - !warned &&
723 - rootSibling.nodeType === ELEMENT_NODE &&
724 - (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)
725 - ) {
726 - warned = true;
727 - warning(
728 - false,
729 - 'render(): Target node has markup rendered by React, but there ' +
730 - 'are unrelated nodes as well. This is most commonly caused by ' +
731 - 'white-space inserted around server-rendered markup.',
732 - );
733 - }
734 - }
735 - container.removeChild(rootSibling);
736 - }
737 - }
738 - if (__DEV__) {
739 - if (shouldHydrate && !forceHydrate && !warnedAboutHydrateAPI) {
740 - warnedAboutHydrateAPI = true;
741 - lowPriorityWarning(
742 - false,
743 - 'render(): Calling ReactDOM.render() to hydrate server-rendered markup ' +
744 - 'will stop working in React v17. Replace the ReactDOM.render() call ' +
745 - 'with ReactDOM.hydrate() if you want React to attach to the server HTML.',
746 - );
747 - }
1060 + // Initial mount
1061 + root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
1062 + container,
1063 + forceHydrate,
1064 + );
1065 + if (typeof callback === 'function') {
1066 + const originalCallback = callback;
1067 + callback = function() {
1068 + const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
1069 + originalCallback.call(instance);
1070 + };
1071 }
749 - const newRoot = DOMRenderer.createContainer(container, shouldHydrate);
750 - root = container._reactRootContainer = newRoot;
1072 // Initial mount should not be batched.
1073 DOMRenderer.unbatchedUpdates(() => {
753 - DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);
1074 + if (parentComponent != null) {
1075 + root.legacy_renderSubtreeIntoContainer(
1076 + parentComponent,
1077 + children,
1078 + callback,
1079 + );
1080 + } else {
1081 + root.render(children, callback);
1082 + }
1083 });
1084 } else {
756 - DOMRenderer.updateContainer(children, root, parentComponent, callback);
1085 + if (typeof callback === 'function') {
1086 + const originalCallback = callback;
1087 + callback = function() {
1088 + const instance = DOMRenderer.getPublicRootInstance(root._internalRoot);
1089 + originalCallback.call(instance);
1090 + };
1091 + }
1092 + // Update
1093 + if (parentComponent != null) {
1094 + root.legacy_renderSubtreeIntoContainer(
1095 + parentComponent,
1096 + children,
1097 + callback,
1098 + );
1099 + } else {
1100 + root.render(children, callback);
1101 + }
1102 }
758 - return DOMRenderer.getPublicRootInstance(root);
1103 + return DOMRenderer.getPublicRootInstance(root._internalRoot);
1104 }
1105
1106 function createPortal(
@@ -771,33 +1116,6 @@ function createPortal(
1116 return ReactPortal.createPortal(children, container, null, key);
1117 }
1118
774 -type ReactRootNode = {
775 - render(children: ReactNodeList, callback: ?() => mixed): void,
776 - unmount(callback: ?() => mixed): void,
777 -
778 - _reactRootContainer: *,
779 -};
780 -
781 -type RootOptions = {
782 - hydrate?: boolean,
783 -};
784 -
785 -function ReactRoot(container: Container, hydrate: boolean) {
786 - const root = DOMRenderer.createContainer(container, hydrate);
787 - this._reactRootContainer = root;
788 -}
789 -ReactRoot.prototype.render = function(
790 - children: ReactNodeList,
791 - callback: ?() => mixed,
792 -): void {
793 - const root = this._reactRootContainer;
794 - DOMRenderer.updateContainer(children, root, null, callback);
795 -};
796 -ReactRoot.prototype.unmount = function(callback) {
797 - const root = this._reactRootContainer;
798 - DOMRenderer.updateContainer(null, root, null, callback);
799 -};
800 -
1119 const ReactDOM: Object = {
1120 createPortal,
1121
@@ -846,7 +1164,13 @@ const ReactDOM: Object = {
1164
1165 hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
1166 // TODO: throw or warn if we couldn't hydrate?
849 - return renderSubtreeIntoContainer(null, element, container, true, callback);
1167 + return legacyRenderSubtreeIntoContainer(
1168 + null,
1169 + element,
1170 + container,
1171 + true,
1172 + callback,
1173 + );
1174 },
1175
1176 render(
@@ -854,7 +1178,7 @@ const ReactDOM: Object = {
1178 container: DOMContainer,
1179 callback: ?Function,
1180 ) {
857 - return renderSubtreeIntoContainer(
1181 + return legacyRenderSubtreeIntoContainer(
1182 null,
1183 element,
1184 container,
@@ -873,7 +1197,7 @@ const ReactDOM: Object = {
1197 parentComponent != null && ReactInstanceMap.has(parentComponent),
1198 'parentComponent must be a valid React Component',
1199 );
876 - return renderSubtreeIntoContainer(
1200 + return legacyRenderSubtreeIntoContainer(
1201 parentComponent,
1202 element,
1203 containerNode,
@@ -902,7 +1226,7 @@ const ReactDOM: Object = {
1226
1227 // Unmount should not be batched.
1228 DOMRenderer.unbatchedUpdates(() => {
905 - renderSubtreeIntoContainer(null, null, container, false, () => {
1229 + legacyRenderSubtreeIntoContainer(null, null, container, false, () => {
1230 container._reactRootContainer = null;
1231 });
1232 });
@@ -960,11 +1284,15 @@ const ReactDOM: Object = {
1284 },
1285 };
1286
1287 +type RootOptions = {
1288 + hydrate?: boolean,
1289 +};
1290 +
1291 if (enableCreateRoot) {
1292 ReactDOM.createRoot = function createRoot(
1293 container: DOMContainer,
1294 options?: RootOptions,
967 - ): ReactRootNode {
1295 + ): ReactRoot {
1296 const hydrate = options != null && options.hydrate === true;
1297 return new ReactRoot(container, hydrate);
1298 };
packages/react-reconciler/src/ReactFiberReconciler.js
+85 -40
@@ -10,6 +10,7 @@
10 import type {Fiber} from './ReactFiber';
11 import type {FiberRoot} from './ReactFiberRoot';
12 import type {ReactNodeList} from 'shared/ReactTypes';
13 +import type {ExpirationTime} from './ReactFiberExpirationTime';
14
15 import {enableAsyncSubtreeAPI} from 'shared/ReactFeatureFlags';
16 import {
@@ -238,12 +239,22 @@ export type Reconciler<C, I, TI> = {
239 container: OpaqueRoot,
240 parentComponent: ?React$Component<any, any>,
241 callback: ?Function,
241 - ): void,
242 + ): ExpirationTime,
243 + updateContainerAtExpirationTime(
244 + element: ReactNodeList,
245 + container: OpaqueRoot,
246 + parentComponent: ?React$Component<any, any>,
247 + expirationTime: ExpirationTime,
248 + callback: ?Function,
249 + ): ExpirationTime,
250 + flushRoot(root: OpaqueRoot, expirationTime: ExpirationTime): void,
251 + requestWork(root: OpaqueRoot, expirationTime: ExpirationTime): void,
252 batchedUpdates<A>(fn: () => A): A,
253 unbatchedUpdates<A>(fn: () => A): A,
254 flushSync<A>(fn: () => A): A,
255 deferredUpdates<A>(fn: () => A): A,
256 injectIntoDevTools(devToolsConfig: DevToolsConfig<I, TI>): boolean,
257 + computeUniqueAsyncExpiration(): ExpirationTime,
258
259 // Used to extract the return value from the initial render. Legacy API.
260 getPublicRootInstance(
@@ -278,17 +289,40 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
289
290 var {
291 computeAsyncExpiration,
292 + computeUniqueAsyncExpiration,
293 computeExpirationForFiber,
294 scheduleWork,
295 + requestWork,
296 + flushRoot,
297 batchedUpdates,
298 unbatchedUpdates,
299 flushSync,
300 deferredUpdates,
301 } = ReactFiberScheduler(config);
302
289 - function scheduleTopLevelUpdate(
303 + function computeRootExpirationTime(current, element) {
304 + let expirationTime;
305 + // Check if the top-level element is an async wrapper component. If so,
306 + // treat updates to the root as async. This is a bit weird but lets us
307 + // avoid a separate `renderAsync` API.
308 + if (
309 + enableAsyncSubtreeAPI &&
310 + element != null &&
311 + (element: any).type != null &&
312 + (element: any).type.prototype != null &&
313 + (element: any).type.prototype.unstable_isAsyncReactComponent === true
314 + ) {
315 + expirationTime = computeAsyncExpiration();
316 + } else {
317 + expirationTime = computeExpirationForFiber(current);
318 + }
319 + return expirationTime;
320 + }
321 +
322 + function scheduleRootUpdate(
323 current: Fiber,
324 element: ReactNodeList,
325 + expirationTime: ExpirationTime,
326 callback: ?Function,
327 ) {
328 if (__DEV__) {
@@ -319,33 +353,50 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
353 );
354 }
355
322 - let expirationTime;
323 - // Check if the top-level element is an async wrapper component. If so,
324 - // treat updates to the root as async. This is a bit weird but lets us
325 - // avoid a separate `renderAsync` API.
326 - if (
327 - enableAsyncSubtreeAPI &&
328 - element != null &&
329 - (element: any).type != null &&
330 - (element: any).type.prototype != null &&
331 - (element: any).type.prototype.unstable_isAsyncReactComponent === true
332 - ) {
333 - expirationTime = computeAsyncExpiration();
334 - } else {
335 - expirationTime = computeExpirationForFiber(current);
336 - }
337 -
356 const update = {
357 expirationTime,
358 partialState: {element},
359 callback,
360 isReplace: false,
361 isForced: false,
344 - nextCallback: null,
362 next: null,
363 };
364 insertUpdateIntoFiber(current, update);
365 scheduleWork(current, expirationTime);
366 +
367 + return expirationTime;
368 + }
369 +
370 + function updateContainerAtExpirationTime(
371 + element: ReactNodeList,
372 + container: OpaqueRoot,
373 + parentComponent: ?React$Component<any, any>,
374 + expirationTime: ExpirationTime,
375 + callback: ?Function,
376 + ) {
377 + // TODO: If this is a nested container, this won't be the root.
378 + const current = container.current;
379 +
380 + if (__DEV__) {
381 + if (ReactFiberInstrumentation.debugTool) {
382 + if (current.alternate === null) {
383 + ReactFiberInstrumentation.debugTool.onMountContainer(container);
384 + } else if (element === null) {
385 + ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
386 + } else {
387 + ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
388 + }
389 + }
390 + }
391 +
392 + const context = getContextForSubtree(parentComponent);
393 + if (container.context === null) {
394 + container.context = context;
395 + } else {
396 + container.pendingContext = context;
397 + }
398 +
399 + return scheduleRootUpdate(current, element, expirationTime, callback);
400 }
401
402 function findHostInstance(fiber: Fiber): PI | null {
@@ -366,31 +417,25 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
417 container: OpaqueRoot,
418 parentComponent: ?React$Component<any, any>,
419 callback: ?Function,
369 - ): void {
370 - // TODO: If this is a nested container, this won't be the root.
420 + ): ExpirationTime {
421 const current = container.current;
422 + const expirationTime = computeRootExpirationTime(current, element);
423 + return updateContainerAtExpirationTime(
424 + element,
425 + container,
426 + parentComponent,
427 + expirationTime,
428 + callback,
429 + );
430 + },
431
373 - if (__DEV__) {
374 - if (ReactFiberInstrumentation.debugTool) {
375 - if (current.alternate === null) {
376 - ReactFiberInstrumentation.debugTool.onMountContainer(container);
377 - } else if (element === null) {
378 - ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
379 - } else {
380 - ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
381 - }
382 - }
383 - }
432 + updateContainerAtExpirationTime,
433
385 - const context = getContextForSubtree(parentComponent);
386 - if (container.context === null) {
387 - container.context = context;
388 - } else {
389 - container.pendingContext = context;
390 - }
434 + flushRoot,
435
392 - scheduleTopLevelUpdate(current, element, callback);
393 - },
436 + requestWork,
437 +
438 + computeUniqueAsyncExpiration,
439
440 batchedUpdates,
441
packages/react-reconciler/src/ReactFiberRoot.js
+13
@@ -13,6 +13,14 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
13 import {createHostRootFiber} from './ReactFiber';
14 import {NoWork} from './ReactFiberExpirationTime';
15
16 +// TODO: This should be lifted into the renderer.
17 +export type Batch = {
18 + _defer: boolean,
19 + _expirationTime: ExpirationTime,
20 + _onComplete: () => mixed,
21 + _next: Batch | null,
22 +};
23 +
24 export type FiberRoot = {
25 // Any additional information from the host associated with this root.
26 containerInfo: any,
@@ -34,6 +42,10 @@ export type FiberRoot = {
42 pendingContext: Object | null,
43 // Determines if we should attempt to hydrate on the initial mount
44 +hydrate: boolean,
45 + // List of top-level batches. This list indicates whether a commit should be
46 + // deferred. Also contains completion callbacks.
47 + // TODO: Lift this into the renderer
48 + firstBatch: Batch | null,
49 // Linked-list of roots
50 nextScheduledRoot: FiberRoot | null,
51 };
@@ -55,6 +67,7 @@ export function createFiberRoot(
67 context: null,
68 pendingContext: null,
69 hydrate,
70 + firstBatch: null,
71 nextScheduledRoot: null,
72 };
73 uninitializedFiber.stateNode = root;
packages/react-reconciler/src/ReactFiberScheduler.js
+98 -12
@@ -9,7 +9,7 @@
9
10 import type {HostConfig, Deadline} from 'react-reconciler';
11 import type {Fiber} from './ReactFiber';
12 -import type {FiberRoot} from './ReactFiberRoot';
12 +import type {FiberRoot, Batch} from './ReactFiberRoot';
13 import type {HydrationContext} from './ReactFiberHydrationContext';
14 import type {ExpirationTime} from './ReactFiberExpirationTime';
15
@@ -187,6 +187,9 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
187 const startTime = now();
188 let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0);
189
190 + // Used to ensure computeUniqueAsyncExpiration is monotonically increases.
191 + let lastUniqueAsyncExpiration: number = 0;
192 +
193 // Represents the expiration time that incoming updates should use. (If this
194 // is NoWork, use the default strategy: async updates in async mode, sync
195 // updates in sync mode.)
@@ -1135,6 +1138,19 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1138 return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs);
1139 }
1140
1141 + // Creates a unique async expiration time.
1142 + function computeUniqueAsyncExpiration(): ExpirationTime {
1143 + let result = computeAsyncExpiration();
1144 + if (result <= lastUniqueAsyncExpiration) {
1145 + // Since we assume the current time monotonically increases, we only hit
1146 + // this branch when computeUniqueAsyncExpiration is fired multiple times
1147 + // within a 200ms window (or whatever the async bucket size is).
1148 + result = lastUniqueAsyncExpiration + 1;
1149 + }
1150 + lastUniqueAsyncExpiration = result;
1151 + return lastUniqueAsyncExpiration;
1152 + }
1153 +
1154 function computeExpirationForFiber(fiber: Fiber) {
1155 let expirationTime;
1156 if (expirationContext !== NoWork) {
@@ -1292,6 +1308,8 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1308 let isBatchingUpdates: boolean = false;
1309 let isUnbatchingUpdates: boolean = false;
1310
1311 + let completedBatches: Array<Batch> | null = null;
1312 +
1313 // Use these to prevent an infinite loop of nested updates
1314 const NESTED_UPDATE_LIMIT = 1000;
1315 let nestedUpdateCount: number = 0;
@@ -1374,7 +1392,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1392 // flush it now.
1393 nextFlushedRoot = root;
1394 nextFlushedExpirationTime = Sync;
1377 - performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
1395 + performWorkOnRoot(root, Sync, recalculateCurrentTime());
1396 }
1397 return;
1398 }
@@ -1486,7 +1504,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1504 nextFlushedExpirationTime <= minExpirationTime) &&
1505 !deadlineDidExpire
1506 ) {
1489 - performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
1507 + performWorkOnRoot(
1508 + nextFlushedRoot,
1509 + nextFlushedExpirationTime,
1510 + recalculateCurrentTime(),
1511 + );
1512 // Find the next highest priority work.
1513 findHighestPriorityRoot();
1514 }
@@ -1509,6 +1531,39 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1531 deadlineDidExpire = false;
1532 nestedUpdateCount = 0;
1533
1534 + finishRendering();
1535 + }
1536 +
1537 + function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1538 + invariant(
1539 + !isRendering,
1540 + 'work.commit(): Cannot commit while already rendering. This likely ' +
1541 + 'means you attempted to commit from inside a lifecycle method.',
1542 + );
1543 + // Perform work on root as if the given expiration time is the current time.
1544 + // This has the effect of synchronously flushing all work up to and
1545 + // including the given time.
1546 + performWorkOnRoot(root, expirationTime, expirationTime);
1547 + finishRendering();
1548 + }
1549 +
1550 + function finishRendering() {
1551 + if (completedBatches !== null) {
1552 + const batches = completedBatches;
1553 + completedBatches = null;
1554 + for (let i = 0; i < batches.length; i++) {
1555 + const batch = batches[i];
1556 + try {
1557 + batch._onComplete();
1558 + } catch (error) {
1559 + if (!hasUnhandledError) {
1560 + hasUnhandledError = true;
1561 + unhandledError = error;
1562 + }
1563 + }
1564 + }
1565 + }
1566 +
1567 if (hasUnhandledError) {
1568 const error = unhandledError;
1569 unhandledError = null;
@@ -1517,7 +1572,11 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1572 }
1573 }
1574
1520 - function performWorkOnRoot(root, expirationTime) {
1575 + function performWorkOnRoot(
1576 + root: FiberRoot,
1577 + expirationTime: ExpirationTime,
1578 + currentTime: ExpirationTime,
1579 + ) {
1580 invariant(
1581 !isRendering,
1582 'performWorkOnRoot was called recursively. This error is likely caused ' +
@@ -1527,20 +1586,18 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1586 isRendering = true;
1587
1588 // Check if this is async work or sync/expired work.
1530 - // TODO: Pass current time as argument to renderRoot, commitRoot
1531 - if (expirationTime <= recalculateCurrentTime()) {
1589 + if (expirationTime <= currentTime) {
1590 // Flush sync work.
1591 let finishedWork = root.finishedWork;
1592 if (finishedWork !== null) {
1593 // This root is already complete. We can commit it.
1536 - root.finishedWork = null;
1537 - root.remainingExpirationTime = commitRoot(finishedWork);
1594 + completeRoot(root, finishedWork, expirationTime);
1595 } else {
1596 root.finishedWork = null;
1597 finishedWork = renderRoot(root, expirationTime);
1598 if (finishedWork !== null) {
1599 // We've completed the root. Commit it.
1543 - root.remainingExpirationTime = commitRoot(finishedWork);
1600 + completeRoot(root, finishedWork, expirationTime);
1601 }
1602 }
1603 } else {
@@ -1548,8 +1605,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1605 let finishedWork = root.finishedWork;
1606 if (finishedWork !== null) {
1607 // This root is already complete. We can commit it.
1551 - root.finishedWork = null;
1552 - root.remainingExpirationTime = commitRoot(finishedWork);
1608 + completeRoot(root, finishedWork, expirationTime);
1609 } else {
1610 root.finishedWork = null;
1611 finishedWork = renderRoot(root, expirationTime);
@@ -1558,7 +1614,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1614 // before committing.
1615 if (!shouldYield()) {
1616 // Still time left. Commit the root.
1561 - root.remainingExpirationTime = commitRoot(finishedWork);
1617 + completeRoot(root, finishedWork, expirationTime);
1618 } else {
1619 // There's no time left. Mark this root as complete. We'll come
1620 // back and commit it later.
@@ -1571,6 +1627,33 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1627 isRendering = false;
1628 }
1629
1630 + function completeRoot(
1631 + root: FiberRoot,
1632 + finishedWork: Fiber,
1633 + expirationTime: ExpirationTime,
1634 + ): void {
1635 + // Check if there's a batch that matches this expiration time.
1636 + const firstBatch = root.firstBatch;
1637 + if (firstBatch !== null && firstBatch._expirationTime <= expirationTime) {
1638 + if (completedBatches === null) {
1639 + completedBatches = [firstBatch];
1640 + } else {
1641 + completedBatches.push(firstBatch);
1642 + }
1643 + if (firstBatch._defer) {
1644 + // This root is blocked from committing by a batch. Unschedule it until
1645 + // we receive another update.
1646 + root.finishedWork = finishedWork;
1647 + root.remainingExpirationTime = NoWork;
1648 + return;
1649 + }
1650 + }
1651 +
1652 + // Commit the root.
1653 + root.finishedWork = null;
1654 + root.remainingExpirationTime = commitRoot(finishedWork);
1655 + }
1656 +
1657 // When working on async work, the reconciler asks the renderer if it should
1658 // yield execution. For DOM, we implement this with requestIdleCallback.
1659 function shouldYield() {
@@ -1654,9 +1737,12 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
1737 computeAsyncExpiration,
1738 computeExpirationForFiber,
1739 scheduleWork,
1740 + requestWork,
1741 + flushRoot,
1742 batchedUpdates,
1743 unbatchedUpdates,
1744 flushSync,
1745 deferredUpdates,
1746 + computeUniqueAsyncExpiration,
1747 };
1748 }
packages/react-test-renderer/src/ReactTestRenderer.js
+1 -1
@@ -607,7 +607,7 @@ var ReactTestRendererFiber = {
607 if (root == null || root.current == null) {
608 return;
609 }
610 - TestRenderer.updateContainer(null, root, null);
610 + TestRenderer.updateContainer(null, root, null, null);
611 container = null;
612 root = null;
613 },