@samitouri / QOS-React / commits / 3bf398bcbc

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,