ReactDOM.unstable_asyncRender creates an async-by-default tree
The default priority of updates in a async tree is LowPriority, rather than SynchronousPriority. Warns if you call unstable_asyncRender on a tree that was created with the normal, sync ReactDOM.render.
Andrew Clark committed
Apr 17, 2017 at 14:54 UTC
3bf398bcbc7849eebfd2cc7b200db41e5c84c6de
4 files changed
+249
-86
src/renderers/dom/fiber/ReactDOMFiber.js
+98
-64
@@ -386,6 +386,7 @@ function renderSubtreeIntoContainer(
386
parentComponent: ?ReactComponent<any, any, any>,
387
children: ReactNodeList,
388
containerNode: DOMContainerElement | Document,
389
+ async: boolean,
390
callback: ?Function,
391
) {
392
validateContainer(containerNode);
@@ -399,84 +400,99 @@ function renderSubtreeIntoContainer(
400
while (container.lastChild) {
401
container.removeChild(container.lastChild);
402
}
402
- const newRoot = DOMRenderer.createContainer(container);
403
- root = container._reactRootContainer = newRoot;
403
+ const newRoot = async
404
+ ? DOMRenderer.createAsyncContainer(container)
405
+ : DOMRenderer.createContainer(container);
406
+ root = (container._reactRootContainer = newRoot);
407
// Initial mount should not be batched.
408
DOMRenderer.unbatchedUpdates(() => {
409
DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);
410
});
411
} else {
409
- DOMRenderer.updateContainer(children, root, parentComponent, callback);
412
+ if (async) {
413
+ DOMRenderer.updateAsyncContainer(children, root, parentComponent, callback);
414
+ } else {
415
+ DOMRenderer.updateContainer(children, root, parentComponent, callback);
416
+ }
417
}
418
return DOMRenderer.getPublicRootInstance(root);
419
}
420
421
+function render(
422
+ element: ReactElement<any>,
423
+ container: DOMContainerElement,
424
+ async: boolean,
425
+ callback: ?Function,
426
+) {
427
+ validateContainer(container);
428
+
429
+ if (ReactFeatureFlags.disableNewFiberFeatures) {
430
+ // Top-level check occurs here instead of inside child reconciler because
431
+ // because requirements vary between renderers. E.g. React Art
432
+ // allows arrays.
433
+ if (!isValidElement(element)) {
434
+ if (typeof element === 'string') {
435
+ invariant(
436
+ false,
437
+ 'ReactDOM.render(): Invalid component element. Instead of ' +
438
+ "passing a string like 'div', pass " +
439
+ "React.createElement('div') or <div />.",
440
+ );
441
+ } else if (typeof element === 'function') {
442
+ invariant(
443
+ false,
444
+ 'ReactDOM.render(): Invalid component element. Instead of ' +
445
+ 'passing a class like Foo, pass React.createElement(Foo) ' +
446
+ 'or <Foo />.',
447
+ );
448
+ } else if (element != null && typeof element.props !== 'undefined') {
449
+ // Check if it quacks like an element
450
+ invariant(
451
+ false,
452
+ 'ReactDOM.render(): Invalid component element. This may be ' +
453
+ 'caused by unintentionally loading two independent copies ' +
454
+ 'of React.',
455
+ );
456
+ } else {
457
+ invariant(false, 'ReactDOM.render(): Invalid component element.');
458
+ }
459
+ }
460
+ }
461
+
462
+ if (__DEV__) {
463
+ const isRootRenderedBySomeReact = !!container._reactRootContainer;
464
+ const rootEl = getReactRootElementInContainer(container);
465
+ const hasNonRootReactChild = !!(rootEl &&
466
+ ReactDOMComponentTree.getInstanceFromNode(rootEl));
467
+
468
+ warning(
469
+ !hasNonRootReactChild || isRootRenderedBySomeReact,
470
+ 'render(...): Replacing React-rendered children with a new root ' +
471
+ 'component. If you intended to update the children of this node, ' +
472
+ 'you should instead have the existing children update their state ' +
473
+ 'and render the new components instead of calling ReactDOM.render.',
474
+ );
475
+
476
+ warning(
477
+ !container.tagName || container.tagName.toUpperCase() !== 'BODY',
478
+ 'render(): Rendering components directly into document.body is ' +
479
+ 'discouraged, since its children are often manipulated by third-party ' +
480
+ 'scripts and browser extensions. This may lead to subtle ' +
481
+ 'reconciliation issues. Try rendering into a container element created ' +
482
+ 'for your app.',
483
+ );
484
+ }
485
+
486
+ return renderSubtreeIntoContainer(null, element, container, async, callback);
487
+}
488
+
489
var ReactDOM = {
490
render(
491
element: ReactElement<any>,
492
container: DOMContainerElement,
493
callback: ?Function,
494
) {
420
- validateContainer(container);
421
-
422
- if (ReactFeatureFlags.disableNewFiberFeatures) {
423
- // Top-level check occurs here instead of inside child reconciler because
424
- // because requirements vary between renderers. E.g. React Art
425
- // allows arrays.
426
- if (!isValidElement(element)) {
427
- if (typeof element === 'string') {
428
- invariant(
429
- false,
430
- 'ReactDOM.render(): Invalid component element. Instead of ' +
431
- "passing a string like 'div', pass " +
432
- "React.createElement('div') or <div />.",
433
- );
434
- } else if (typeof element === 'function') {
435
- invariant(
436
- false,
437
- 'ReactDOM.render(): Invalid component element. Instead of ' +
438
- 'passing a class like Foo, pass React.createElement(Foo) ' +
439
- 'or <Foo />.',
440
- );
441
- } else if (element != null && typeof element.props !== 'undefined') {
442
- // Check if it quacks like an element
443
- invariant(
444
- false,
445
- 'ReactDOM.render(): Invalid component element. This may be ' +
446
- 'caused by unintentionally loading two independent copies ' +
447
- 'of React.',
448
- );
449
- } else {
450
- invariant(false, 'ReactDOM.render(): Invalid component element.');
451
- }
452
- }
453
- }
454
-
455
- if (__DEV__) {
456
- const isRootRenderedBySomeReact = !!container._reactRootContainer;
457
- const rootEl = getReactRootElementInContainer(container);
458
- const hasNonRootReactChild = !!(rootEl &&
459
- ReactDOMComponentTree.getInstanceFromNode(rootEl));
460
-
461
- warning(
462
- !hasNonRootReactChild || isRootRenderedBySomeReact,
463
- 'render(...): Replacing React-rendered children with a new root ' +
464
- 'component. If you intended to update the children of this node, ' +
465
- 'you should instead have the existing children update their state ' +
466
- 'and render the new components instead of calling ReactDOM.render.',
467
- );
468
-
469
- warning(
470
- !container.tagName || container.tagName.toUpperCase() !== 'BODY',
471
- 'render(): Rendering components directly into document.body is ' +
472
- 'discouraged, since its children are often manipulated by third-party ' +
473
- 'scripts and browser extensions. This may lead to subtle ' +
474
- 'reconciliation issues. Try rendering into a container element created ' +
475
- 'for your app.',
476
- );
477
- }
478
-
479
- return renderSubtreeIntoContainer(null, element, container, callback);
495
+ return render(element, container, false, callback);
496
},
497
498
unstable_renderSubtreeIntoContainer(
@@ -493,10 +509,13 @@ var ReactDOM = {
509
parentComponent,
510
element,
511
containerNode,
512
+ false,
513
callback,
514
);
515
},
516
517
+ unstable_asyncRender: (null : ?(element: ReactElement<any>, container: DOMContainerElement, callback: ?Function) => *),
518
+
519
unmountComponentAtNode(container: DOMContainerElement) {
520
invariant(
521
isValidContainer(container),
@@ -518,7 +537,7 @@ var ReactDOM = {
537
538
// Unmount should not be batched.
539
return DOMRenderer.unbatchedUpdates(() => {
521
- return renderSubtreeIntoContainer(null, null, container, () => {
540
+ return renderSubtreeIntoContainer(null, null, container, false, () => {
541
container._reactRootContainer = null;
542
});
543
});
@@ -552,6 +571,21 @@ var ReactDOM = {
571
},
572
};
573
574
+if (ReactDOMFeatureFlags.enableAsyncSubtreeAPI) {
575
+ ReactDOM.unstable_asyncRender = function(
576
+ element: ReactElement<any>,
577
+ container: DOMContainerElement,
578
+ callback: ?Function,
579
+ ) {
580
+ return render(element, container, true, callback);
581
+ };
582
+} else {
583
+ // We set this to null on the ReactDOM export, then delete if the feature
584
+ // flag is not enabled so that it's undiscoverable.
585
+ // TODO: Is there a better way to satisfy Flow?
586
+ delete ReactDOM.unstable_asyncRender;
587
+}
588
+
589
if (typeof injectInternals === 'function') {
590
injectInternals({
591
findFiberByHostInstance: ReactDOMComponentTree.getClosestInstanceFromNode,
src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js
new
+75
@@ -0,0 +1,75 @@
1
+var React = require('react');
2
+var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
3
+
4
+var ReactDOM;
5
+
6
+describe('ReactDOMFiberAsync', () => {
7
+ var container;
8
+
9
+ beforeEach(() => {
10
+ container = document.createElement('div');
11
+ ReactDOM = require('react-dom');
12
+ });
13
+
14
+ it('renders synchronously by default', () => {
15
+ var ops = [];
16
+ ReactDOM.render(<div>Hi</div>, container, () => {
17
+ ops.push(container.textContent);
18
+ });
19
+ ReactDOM.render(<div>Bye</div>, container, () => {
20
+ ops.push(container.textContent);
21
+ });
22
+ expect(ops).toEqual(['Hi', 'Bye']);
23
+ });
24
+
25
+ if (ReactDOMFeatureFlags.useFiber) {
26
+ it('throws when calling async APIs when feature flag is disabled', () => {
27
+ expect(() => {
28
+ ReactDOM.unstable_asyncRender(<div>Hi</div>, container);
29
+ }).toThrow('ReactDOM.unstable_asyncRender is not a function');
30
+ });
31
+
32
+ describe('with feature flag enabled', () => {
33
+ beforeEach(() => {
34
+ jest.resetModules();
35
+ ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
36
+ container = document.createElement('div');
37
+ ReactDOMFeatureFlags.enableAsyncSubtreeAPI = true;
38
+ ReactDOM = require('react-dom');
39
+ });
40
+
41
+ it('unstable_asyncRender creates an async tree', () => {
42
+ ReactDOM.unstable_asyncRender(<div>Hi</div>, container);
43
+ expect(container.textContent).toEqual('');
44
+ jest.runAllTimers();
45
+ expect(container.textContent).toEqual('Hi');
46
+
47
+ ReactDOM.unstable_asyncRender(<div>Bye</div>, container);
48
+ expect(container.textContent).toEqual('Hi');
49
+ jest.runAllTimers();
50
+ expect(container.textContent).toEqual('Bye');
51
+ });
52
+
53
+ it('updates inside an async tree are async by default', () => {
54
+ let instance;
55
+ class Component extends React.Component {
56
+ state = {step: 0};
57
+ render() {
58
+ instance = this;
59
+ return <div>{this.state.step}</div>;
60
+ }
61
+ }
62
+
63
+ ReactDOM.unstable_asyncRender(<Component />, container);
64
+ expect(container.textContent).toEqual('');
65
+ jest.runAllTimers();
66
+ expect(container.textContent).toEqual('0');
67
+
68
+ instance.setState({step: 1});
69
+ expect(container.textContent).toEqual('0');
70
+ jest.runAllTimers();
71
+ expect(container.textContent).toEqual('1');
72
+ });
73
+ });
74
+ }
75
+});
src/renderers/dom/shared/ReactDOMFeatureFlags.js
+1
@@ -13,6 +13,7 @@
13
14
var ReactDOMFeatureFlags = {
15
fiberAsyncScheduling: false,
16
+ enableAsyncSubtreeAPI: false,
17
useCreateElement: true,
18
useFiber: true,
19
};
src/renderers/shared/fiber/ReactFiberReconciler.js
+75
-22
@@ -119,10 +119,18 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
119
120
export type Reconciler<C, I, TI> = {
121
createContainer(containerInfo: C): OpaqueRoot,
122
+ createAsyncContainer(containerInfo: C): OpaqueRoot,
123
updateContainer(
124
element: ReactNodeList,
125
container: OpaqueRoot,
126
parentComponent: ?ReactComponent<any, any, any>,
127
+ callback: ?Function,
128
+ ): void,
129
+ updateAsyncContainer(
130
+ element: ReactNodeList,
131
+ container: OpaqueRoot,
132
+ parentComponent: ?ReactComponent<any, any, any>,
133
+ callback: ?Function,
134
): void,
135
performWithPriority(priorityLevel: PriorityLevel, fn: Function): void,
136
batchedUpdates<A>(fn: () => A): A,
@@ -195,40 +203,85 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
203
scheduleUpdate(current, priorityLevel);
204
}
205
206
+ function updateContainer(
207
+ element: ReactNodeList,
208
+ container: OpaqueRoot,
209
+ parentComponent: ?ReactComponent<any, any, any>,
210
+ async: boolean,
211
+ callback: ?Function,
212
+ ) {
213
+ // TODO: Make messages more user-friendly?
214
+ if (__DEV__) {
215
+ warning(
216
+ !async || (container.current.contextTag & AsyncUpdates),
217
+ 'Attempted to schedule an asynchronous update on a sync container.'
218
+ );
219
+ }
220
+
221
+ // TODO: If this is a nested container, this won't be the root.
222
+ const current = container.current;
223
+
224
+ if (__DEV__) {
225
+ if (ReactFiberInstrumentation.debugTool) {
226
+ if (current.alternate === null) {
227
+ ReactFiberInstrumentation.debugTool.onMountContainer(container);
228
+ } else if (element === null) {
229
+ ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
230
+ } else {
231
+ ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
232
+ }
233
+ }
234
+ }
235
+
236
+ const context = getContextForSubtree(parentComponent);
237
+ if (container.context === null) {
238
+ container.context = context;
239
+ } else {
240
+ container.pendingContext = context;
241
+ }
242
+
243
+ scheduleTopLevelUpdate(current, element, callback);
244
+ }
245
+
246
return {
247
createContainer(containerInfo: C): OpaqueRoot {
248
return createFiberRoot(containerInfo);
249
},
250
251
+ createAsyncContainer(containerInfo: C): OpaqueRoot {
252
+ const fiberRoot = createFiberRoot(containerInfo);
253
+ fiberRoot.current.contextTag |= AsyncUpdates;
254
+ return fiberRoot;
255
+ },
256
+
257
updateContainer(
258
element: ReactNodeList,
259
container: OpaqueRoot,
260
parentComponent: ?ReactComponent<any, any, any>,
261
callback: ?Function,
262
): void {
209
- // TODO: If this is a nested container, this won't be the root.
210
- const current = container.current;
211
-
212
- if (__DEV__) {
213
- if (ReactFiberInstrumentation.debugTool) {
214
- if (current.alternate === null) {
215
- ReactFiberInstrumentation.debugTool.onMountContainer(container);
216
- } else if (element === null) {
217
- ReactFiberInstrumentation.debugTool.onUnmountContainer(container);
218
- } else {
219
- ReactFiberInstrumentation.debugTool.onUpdateContainer(container);
220
- }
221
- }
222
- }
223
-
224
- const context = getContextForSubtree(parentComponent);
225
- if (container.context === null) {
226
- container.context = context;
227
- } else {
228
- container.pendingContext = context;
229
- }
263
+ updateContainer(
264
+ element,
265
+ container,
266
+ parentComponent,
267
+ false, // async = false
268
+ callback,
269
+ );
270
+ },
271
231
- scheduleTopLevelUpdate(current, element, callback);
272
+ updateAsyncContainer(
273
+ element: ReactNodeList,
274
+ container: OpaqueRoot,
275
+ parentComponent: ?ReactComponent<any, any, any>,
276
+ callback: ?Function,
277
+ ) {
278
+ updateContainer(
279
+ element,
280
+ container,
281
+ parentComponent,
282
+ true, // async = true
283
+ callback,
284
+ );
285
},
286
287
performWithPriority,