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
},