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.
This commit is contained in:
Andrew Clark
2017-04-17 14:54:59 -07:00
parent 6f8fec4b80
commit 3bf398bcbc
4 changed files with 249 additions and 86 deletions
+98 -64
View File
@@ -386,6 +386,7 @@ function renderSubtreeIntoContainer(
parentComponent: ?ReactComponent<any, any, any>,
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<any>,
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 <div />.",
);
} else if (typeof element === 'function') {
invariant(
false,
'ReactDOM.render(): Invalid component element. Instead of ' +
'passing a class like Foo, pass React.createElement(Foo) ' +
'or <Foo />.',
);
} 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<any>,
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 <div />.",
);
} else if (typeof element === 'function') {
invariant(
false,
'ReactDOM.render(): Invalid component element. Instead of ' +
'passing a class like Foo, pass React.createElement(Foo) ' +
'or <Foo />.',
);
} 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<any>, 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<any>,
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,
@@ -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(<div>Hi</div>, container, () => {
ops.push(container.textContent);
});
ReactDOM.render(<div>Bye</div>, 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(<div>Hi</div>, 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(<div>Hi</div>, container);
expect(container.textContent).toEqual('');
jest.runAllTimers();
expect(container.textContent).toEqual('Hi');
ReactDOM.unstable_asyncRender(<div>Bye</div>, 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 <div>{this.state.step}</div>;
}
}
ReactDOM.unstable_asyncRender(<Component />, 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');
});
});
}
});
@@ -13,6 +13,7 @@
var ReactDOMFeatureFlags = {
fiberAsyncScheduling: false,
enableAsyncSubtreeAPI: false,
useCreateElement: true,
useFiber: true,
};
@@ -119,10 +119,18 @@ export type HostConfig<T, P, I, TI, PI, C, CX, PL> = {
export type Reconciler<C, I, TI> = {
createContainer(containerInfo: C): OpaqueRoot,
createAsyncContainer(containerInfo: C): OpaqueRoot,
updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?ReactComponent<any, any, any>,
callback: ?Function,
): void,
updateAsyncContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?ReactComponent<any, any, any>,
callback: ?Function,
): void,
performWithPriority(priorityLevel: PriorityLevel, fn: Function): void,
batchedUpdates<A>(fn: () => A): A,
@@ -195,40 +203,85 @@ module.exports = function<T, P, I, TI, PI, C, CX, PL>(
scheduleUpdate(current, priorityLevel);
}
function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?ReactComponent<any, any, any>,
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<any, any, any>,
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<any, any, any>,
callback: ?Function,
) {
updateContainer(
element,
container,
parentComponent,
true, // async = true
callback,
);
},
performWithPriority,