From 3bf398bcbc7849eebfd2cc7b200db41e5c84c6de Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 17 Apr 2017 14:54:59 -0700 Subject: [PATCH] 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. --- src/renderers/dom/fiber/ReactDOMFiber.js | 162 +++++++++++------- .../__tests__/ReactDOMFiberAsync-test.js | 75 ++++++++ .../dom/shared/ReactDOMFeatureFlags.js | 1 + .../shared/fiber/ReactFiberReconciler.js | 97 ++++++++--- 4 files changed, 249 insertions(+), 86 deletions(-) create mode 100644 src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 77b980cd36..8486afcc82 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -386,6 +386,7 @@ function renderSubtreeIntoContainer( parentComponent: ?ReactComponent, children: ReactNodeList, containerNode: DOMContainerElement | Document, + async: boolean, callback: ?Function, ) { validateContainer(containerNode); @@ -399,84 +400,99 @@ function renderSubtreeIntoContainer( while (container.lastChild) { container.removeChild(container.lastChild); } - const newRoot = DOMRenderer.createContainer(container); - root = container._reactRootContainer = newRoot; + const newRoot = async + ? DOMRenderer.createAsyncContainer(container) + : DOMRenderer.createContainer(container); + root = (container._reactRootContainer = newRoot); // Initial mount should not be batched. DOMRenderer.unbatchedUpdates(() => { DOMRenderer.updateContainer(children, newRoot, parentComponent, callback); }); } else { - DOMRenderer.updateContainer(children, root, parentComponent, callback); + if (async) { + DOMRenderer.updateAsyncContainer(children, root, parentComponent, callback); + } else { + DOMRenderer.updateContainer(children, root, parentComponent, callback); + } } return DOMRenderer.getPublicRootInstance(root); } +function render( + element: ReactElement, + container: DOMContainerElement, + async: boolean, + callback: ?Function, +) { + validateContainer(container); + + if (ReactFeatureFlags.disableNewFiberFeatures) { + // Top-level check occurs here instead of inside child reconciler because + // because requirements vary between renderers. E.g. React Art + // allows arrays. + if (!isValidElement(element)) { + if (typeof element === 'string') { + invariant( + false, + 'ReactDOM.render(): Invalid component element. Instead of ' + + "passing a string like 'div', pass " + + "React.createElement('div') or
.", + ); + } else if (typeof element === 'function') { + invariant( + false, + 'ReactDOM.render(): Invalid component element. Instead of ' + + 'passing a class like Foo, pass React.createElement(Foo) ' + + 'or .', + ); + } else if (element != null && typeof element.props !== 'undefined') { + // Check if it quacks like an element + invariant( + false, + 'ReactDOM.render(): Invalid component element. This may be ' + + 'caused by unintentionally loading two independent copies ' + + 'of React.', + ); + } else { + invariant(false, 'ReactDOM.render(): Invalid component element.'); + } + } + } + + if (__DEV__) { + const isRootRenderedBySomeReact = !!container._reactRootContainer; + const rootEl = getReactRootElementInContainer(container); + const hasNonRootReactChild = !!(rootEl && + ReactDOMComponentTree.getInstanceFromNode(rootEl)); + + warning( + !hasNonRootReactChild || isRootRenderedBySomeReact, + 'render(...): Replacing React-rendered children with a new root ' + + 'component. If you intended to update the children of this node, ' + + 'you should instead have the existing children update their state ' + + 'and render the new components instead of calling ReactDOM.render.', + ); + + warning( + !container.tagName || container.tagName.toUpperCase() !== 'BODY', + 'render(): Rendering components directly into document.body is ' + + 'discouraged, since its children are often manipulated by third-party ' + + 'scripts and browser extensions. This may lead to subtle ' + + 'reconciliation issues. Try rendering into a container element created ' + + 'for your app.', + ); + } + + return renderSubtreeIntoContainer(null, element, container, async, callback); +} + var ReactDOM = { render( element: ReactElement, container: DOMContainerElement, callback: ?Function, ) { - validateContainer(container); - - if (ReactFeatureFlags.disableNewFiberFeatures) { - // Top-level check occurs here instead of inside child reconciler because - // because requirements vary between renderers. E.g. React Art - // allows arrays. - if (!isValidElement(element)) { - if (typeof element === 'string') { - invariant( - false, - 'ReactDOM.render(): Invalid component element. Instead of ' + - "passing a string like 'div', pass " + - "React.createElement('div') or
.", - ); - } else if (typeof element === 'function') { - invariant( - false, - 'ReactDOM.render(): Invalid component element. Instead of ' + - 'passing a class like Foo, pass React.createElement(Foo) ' + - 'or .', - ); - } else if (element != null && typeof element.props !== 'undefined') { - // Check if it quacks like an element - invariant( - false, - 'ReactDOM.render(): Invalid component element. This may be ' + - 'caused by unintentionally loading two independent copies ' + - 'of React.', - ); - } else { - invariant(false, 'ReactDOM.render(): Invalid component element.'); - } - } - } - - if (__DEV__) { - const isRootRenderedBySomeReact = !!container._reactRootContainer; - const rootEl = getReactRootElementInContainer(container); - const hasNonRootReactChild = !!(rootEl && - ReactDOMComponentTree.getInstanceFromNode(rootEl)); - - warning( - !hasNonRootReactChild || isRootRenderedBySomeReact, - 'render(...): Replacing React-rendered children with a new root ' + - 'component. If you intended to update the children of this node, ' + - 'you should instead have the existing children update their state ' + - 'and render the new components instead of calling ReactDOM.render.', - ); - - warning( - !container.tagName || container.tagName.toUpperCase() !== 'BODY', - 'render(): Rendering components directly into document.body is ' + - 'discouraged, since its children are often manipulated by third-party ' + - 'scripts and browser extensions. This may lead to subtle ' + - 'reconciliation issues. Try rendering into a container element created ' + - 'for your app.', - ); - } - - return renderSubtreeIntoContainer(null, element, container, callback); + return render(element, container, false, callback); }, unstable_renderSubtreeIntoContainer( @@ -493,10 +509,13 @@ var ReactDOM = { parentComponent, element, containerNode, + false, callback, ); }, + unstable_asyncRender: (null : ?(element: ReactElement, container: DOMContainerElement, callback: ?Function) => *), + unmountComponentAtNode(container: DOMContainerElement) { invariant( isValidContainer(container), @@ -518,7 +537,7 @@ var ReactDOM = { // Unmount should not be batched. return DOMRenderer.unbatchedUpdates(() => { - return renderSubtreeIntoContainer(null, null, container, () => { + return renderSubtreeIntoContainer(null, null, container, false, () => { container._reactRootContainer = null; }); }); @@ -552,6 +571,21 @@ var ReactDOM = { }, }; +if (ReactDOMFeatureFlags.enableAsyncSubtreeAPI) { + ReactDOM.unstable_asyncRender = function( + element: ReactElement, + container: DOMContainerElement, + callback: ?Function, + ) { + return render(element, container, true, callback); + }; +} else { + // We set this to null on the ReactDOM export, then delete if the feature + // flag is not enabled so that it's undiscoverable. + // TODO: Is there a better way to satisfy Flow? + delete ReactDOM.unstable_asyncRender; +} + if (typeof injectInternals === 'function') { injectInternals({ findFiberByHostInstance: ReactDOMComponentTree.getClosestInstanceFromNode, diff --git a/src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js b/src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js new file mode 100644 index 0000000000..2d9f6d9d60 --- /dev/null +++ b/src/renderers/dom/fiber/__tests__/ReactDOMFiberAsync-test.js @@ -0,0 +1,75 @@ +var React = require('react'); +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + +var ReactDOM; + +describe('ReactDOMFiberAsync', () => { + var container; + + beforeEach(() => { + container = document.createElement('div'); + ReactDOM = require('react-dom'); + }); + + it('renders synchronously by default', () => { + var ops = []; + ReactDOM.render(
Hi
, container, () => { + ops.push(container.textContent); + }); + ReactDOM.render(
Bye
, container, () => { + ops.push(container.textContent); + }); + expect(ops).toEqual(['Hi', 'Bye']); + }); + + if (ReactDOMFeatureFlags.useFiber) { + it('throws when calling async APIs when feature flag is disabled', () => { + expect(() => { + ReactDOM.unstable_asyncRender(
Hi
, container); + }).toThrow('ReactDOM.unstable_asyncRender is not a function'); + }); + + describe('with feature flag enabled', () => { + beforeEach(() => { + jest.resetModules(); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + container = document.createElement('div'); + ReactDOMFeatureFlags.enableAsyncSubtreeAPI = true; + ReactDOM = require('react-dom'); + }); + + it('unstable_asyncRender creates an async tree', () => { + ReactDOM.unstable_asyncRender(
Hi
, container); + expect(container.textContent).toEqual(''); + jest.runAllTimers(); + expect(container.textContent).toEqual('Hi'); + + ReactDOM.unstable_asyncRender(
Bye
, container); + expect(container.textContent).toEqual('Hi'); + jest.runAllTimers(); + expect(container.textContent).toEqual('Bye'); + }); + + it('updates inside an async tree are async by default', () => { + let instance; + class Component extends React.Component { + state = {step: 0}; + render() { + instance = this; + return
{this.state.step}
; + } + } + + ReactDOM.unstable_asyncRender(, container); + expect(container.textContent).toEqual(''); + jest.runAllTimers(); + expect(container.textContent).toEqual('0'); + + instance.setState({step: 1}); + expect(container.textContent).toEqual('0'); + jest.runAllTimers(); + expect(container.textContent).toEqual('1'); + }); + }); + } +}); diff --git a/src/renderers/dom/shared/ReactDOMFeatureFlags.js b/src/renderers/dom/shared/ReactDOMFeatureFlags.js index 1e21bca758..7fb34ceb23 100644 --- a/src/renderers/dom/shared/ReactDOMFeatureFlags.js +++ b/src/renderers/dom/shared/ReactDOMFeatureFlags.js @@ -13,6 +13,7 @@ var ReactDOMFeatureFlags = { fiberAsyncScheduling: false, + enableAsyncSubtreeAPI: false, useCreateElement: true, useFiber: true, }; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 1cb06243fe..236c481cdf 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -119,10 +119,18 @@ export type HostConfig = { export type Reconciler = { createContainer(containerInfo: C): OpaqueRoot, + createAsyncContainer(containerInfo: C): OpaqueRoot, updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?ReactComponent, + callback: ?Function, + ): void, + updateAsyncContainer( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?ReactComponent, + callback: ?Function, ): void, performWithPriority(priorityLevel: PriorityLevel, fn: Function): void, batchedUpdates(fn: () => A): A, @@ -195,40 +203,85 @@ module.exports = function( scheduleUpdate(current, priorityLevel); } + function updateContainer( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?ReactComponent, + async: boolean, + callback: ?Function, + ) { + // TODO: Make messages more user-friendly? + if (__DEV__) { + warning( + !async || (container.current.contextTag & AsyncUpdates), + 'Attempted to schedule an asynchronous update on a sync container.' + ); + } + + // TODO: If this is a nested container, this won't be the root. + const current = container.current; + + if (__DEV__) { + if (ReactFiberInstrumentation.debugTool) { + if (current.alternate === null) { + ReactFiberInstrumentation.debugTool.onMountContainer(container); + } else if (element === null) { + ReactFiberInstrumentation.debugTool.onUnmountContainer(container); + } else { + ReactFiberInstrumentation.debugTool.onUpdateContainer(container); + } + } + } + + const context = getContextForSubtree(parentComponent); + if (container.context === null) { + container.context = context; + } else { + container.pendingContext = context; + } + + scheduleTopLevelUpdate(current, element, callback); + } + return { createContainer(containerInfo: C): OpaqueRoot { return createFiberRoot(containerInfo); }, + createAsyncContainer(containerInfo: C): OpaqueRoot { + const fiberRoot = createFiberRoot(containerInfo); + fiberRoot.current.contextTag |= AsyncUpdates; + return fiberRoot; + }, + updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?ReactComponent, callback: ?Function, ): void { - // TODO: If this is a nested container, this won't be the root. - const current = container.current; + updateContainer( + element, + container, + parentComponent, + false, // async = false + callback, + ); + }, - if (__DEV__) { - if (ReactFiberInstrumentation.debugTool) { - if (current.alternate === null) { - ReactFiberInstrumentation.debugTool.onMountContainer(container); - } else if (element === null) { - ReactFiberInstrumentation.debugTool.onUnmountContainer(container); - } else { - ReactFiberInstrumentation.debugTool.onUpdateContainer(container); - } - } - } - - const context = getContextForSubtree(parentComponent); - if (container.context === null) { - container.context = context; - } else { - container.pendingContext = context; - } - - scheduleTopLevelUpdate(current, element, callback); + updateAsyncContainer( + element: ReactNodeList, + container: OpaqueRoot, + parentComponent: ?ReactComponent, + callback: ?Function, + ) { + updateContainer( + element, + container, + parentComponent, + true, // async = true + callback, + ); }, performWithPriority,