mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user