mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Prepare new composite child before removing old (#8572)
This matches what we do in Fiber -- and doing it this way is the only way we can prepare new views in the background before unmounting old ones.
In particular, this breaks this pattern:
```js
class Child1 extends React.Component {
render() { ... }
componentWillMount() {
this.props.registerChild(this);
}
componentWillUnmount() {
this.props.unregisterChild();
}
}
class Child2 extends React.Component {
render() { ... }
componentWillMount() {
this.props.registerChild(this);
}
componentWillUnmount() {
this.props.unregisterChild();
}
}
class Parent extends React.Component {
render() {
return (
showChild1 ?
<Child1
registerChild={(child) => this.registered = child}
unregisterChild={() => this.registered = null}
/> :
<Child2
registerChild={(child) => this.registered = child}
unregisterChild={() => this.registered = null}
/>
);
}
}
```
Previously, `this.registered` would always be set -- now, after a rerender, `this.registered` gets stuck at null because the old child's componentWillUnmount runs *after* the new child's componentWillMount.
A correct fix here is to use componentDidMount rather than componentWillMount. (In general, componentWillMount should not have side effects.) If Parent stored a list or set of registered children instead, there would also be no issue.
This commit is contained in:
@@ -1318,6 +1318,7 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js
|
||||
* should support objects with prototypes as state
|
||||
* should not warn about unmounting during unmounting
|
||||
* should only call componentWillUnmount once
|
||||
* prepares new child before unmounting old
|
||||
|
||||
src/renderers/shared/shared/__tests__/ReactCompositeComponentDOMMinimalism-test.js
|
||||
* should not render extra nodes for non-interpolated text
|
||||
@@ -1405,6 +1406,7 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
|
||||
* should NOT replace children with different owners
|
||||
* should replace children with different keys
|
||||
* should reorder bailed-out children
|
||||
* prepares new children before unmounting old
|
||||
|
||||
src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js
|
||||
* should reset internal state if removed then readded in an array
|
||||
|
||||
@@ -1358,4 +1358,45 @@ describe('ReactCompositeComponent', () => {
|
||||
expect(count).toBe(1);
|
||||
});
|
||||
|
||||
it('prepares new child before unmounting old', () => {
|
||||
var log = [];
|
||||
|
||||
class Spy extends React.Component {
|
||||
componentWillMount() {
|
||||
log.push(this.props.name + ' componentWillMount');
|
||||
}
|
||||
render() {
|
||||
log.push(this.props.name + ' render');
|
||||
return <div />;
|
||||
}
|
||||
componentDidMount() {
|
||||
log.push(this.props.name + ' componentDidMount');
|
||||
}
|
||||
componentWillUnmount() {
|
||||
log.push(this.props.name + ' componentWillUnmount');
|
||||
}
|
||||
}
|
||||
|
||||
class Wrapper extends React.Component {
|
||||
render() {
|
||||
return <Spy key={this.props.name} name={this.props.name} />;
|
||||
}
|
||||
}
|
||||
|
||||
var container = document.createElement('div');
|
||||
ReactDOM.render(<Wrapper name="A" />, container);
|
||||
ReactDOM.render(<Wrapper name="B" />, container);
|
||||
|
||||
expect(log).toEqual([
|
||||
'A componentWillMount',
|
||||
'A render',
|
||||
'A componentDidMount',
|
||||
|
||||
'B componentWillMount',
|
||||
'B render',
|
||||
'A componentWillUnmount',
|
||||
'B componentDidMount',
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -18,11 +18,13 @@ describe('ReactMultiChild', () => {
|
||||
|
||||
var React;
|
||||
var ReactDOM;
|
||||
var ReactDOMFeatureFlags;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModuleRegistry();
|
||||
React = require('React');
|
||||
ReactDOM = require('ReactDOM');
|
||||
ReactDOMFeatureFlags = require('ReactDOMFeatureFlags');
|
||||
});
|
||||
|
||||
describe('reconciliation', () => {
|
||||
@@ -254,4 +256,78 @@ describe('ReactMultiChild', () => {
|
||||
ReactDOM.render(<Letters letters="EHCjpdTUuiybDvhRJwZt" />, container);
|
||||
expect(container.textContent).toBe('EHCjpdTUuiybDvhRJwZt');
|
||||
});
|
||||
|
||||
it('prepares new children before unmounting old', () => {
|
||||
var log = [];
|
||||
|
||||
class Spy extends React.Component {
|
||||
componentWillMount() {
|
||||
log.push(this.props.name + ' componentWillMount');
|
||||
}
|
||||
render() {
|
||||
log.push(this.props.name + ' render');
|
||||
return <div />;
|
||||
}
|
||||
componentDidMount() {
|
||||
log.push(this.props.name + ' componentDidMount');
|
||||
}
|
||||
componentWillUnmount() {
|
||||
log.push(this.props.name + ' componentWillUnmount');
|
||||
}
|
||||
}
|
||||
|
||||
// These are reference-unequal so they will be swapped even if they have
|
||||
// matching keys
|
||||
var SpyA = (props) => <Spy {...props} />;
|
||||
var SpyB = (props) => <Spy {...props} />;
|
||||
|
||||
var container = document.createElement('div');
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<SpyA key="one" name="oneA" />
|
||||
<SpyA key="two" name="twoA" />
|
||||
</div>,
|
||||
container
|
||||
);
|
||||
ReactDOM.render(
|
||||
<div>
|
||||
<SpyB key="one" name="oneB" />
|
||||
<SpyB key="two" name="twoB" />
|
||||
</div>,
|
||||
container
|
||||
);
|
||||
|
||||
expect(log).toEqual([
|
||||
'oneA componentWillMount',
|
||||
'oneA render',
|
||||
'twoA componentWillMount',
|
||||
'twoA render',
|
||||
'oneA componentDidMount',
|
||||
'twoA componentDidMount',
|
||||
|
||||
...(
|
||||
ReactDOMFeatureFlags.useFiber ?
|
||||
[
|
||||
'oneB componentWillMount',
|
||||
'oneB render',
|
||||
'twoB componentWillMount',
|
||||
'twoB render',
|
||||
'oneA componentWillUnmount',
|
||||
'twoA componentWillUnmount',
|
||||
] :
|
||||
[
|
||||
'oneB componentWillMount',
|
||||
'oneB render',
|
||||
'oneA componentWillUnmount',
|
||||
'twoB componentWillMount',
|
||||
'twoB render',
|
||||
'twoA componentWillUnmount',
|
||||
]
|
||||
),
|
||||
|
||||
'oneB componentDidMount',
|
||||
'twoB componentDidMount',
|
||||
]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -11,10 +11,11 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var KeyEscapeUtils = require('KeyEscapeUtils');
|
||||
var ReactFeatureFlags = require('ReactFeatureFlags');
|
||||
var ReactReconciler = require('ReactReconciler');
|
||||
|
||||
var instantiateReactComponent = require('instantiateReactComponent');
|
||||
var KeyEscapeUtils = require('KeyEscapeUtils');
|
||||
var shouldUpdateReactComponent = require('shouldUpdateReactComponent');
|
||||
var traverseAllChildren = require('traverseAllChildren');
|
||||
var warning = require('warning');
|
||||
@@ -144,7 +145,10 @@ var ReactChildReconciler = {
|
||||
);
|
||||
nextChildren[name] = prevChild;
|
||||
} else {
|
||||
if (prevChild) {
|
||||
if (
|
||||
!ReactFeatureFlags.prepareNewChildrenBeforeUnmountInStack &&
|
||||
prevChild
|
||||
) {
|
||||
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
|
||||
ReactReconciler.unmountComponent(
|
||||
prevChild,
|
||||
@@ -166,6 +170,17 @@ var ReactChildReconciler = {
|
||||
selfDebugID
|
||||
);
|
||||
mountImages.push(nextChildMountImage);
|
||||
if (
|
||||
ReactFeatureFlags.prepareNewChildrenBeforeUnmountInStack &&
|
||||
prevChild
|
||||
) {
|
||||
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
|
||||
ReactReconciler.unmountComponent(
|
||||
prevChild,
|
||||
false, /* safely */
|
||||
false /* skipLifecycle */
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unmount children that are no longer present.
|
||||
|
||||
@@ -15,6 +15,7 @@ var React = require('React');
|
||||
var ReactComponentEnvironment = require('ReactComponentEnvironment');
|
||||
var ReactCurrentOwner = require('ReactCurrentOwner');
|
||||
var ReactErrorUtils = require('ReactErrorUtils');
|
||||
var ReactFeatureFlags = require('ReactFeatureFlags');
|
||||
var ReactInstanceMap = require('ReactInstanceMap');
|
||||
var ReactInstrumentation = require('ReactInstrumentation');
|
||||
var ReactNodeTypes = require('ReactNodeTypes');
|
||||
@@ -1109,11 +1110,14 @@ var ReactCompositeComponent = {
|
||||
);
|
||||
} else {
|
||||
var oldHostNode = ReactReconciler.getHostNode(prevComponentInstance);
|
||||
ReactReconciler.unmountComponent(
|
||||
prevComponentInstance,
|
||||
safely,
|
||||
false /* skipLifecycle */
|
||||
);
|
||||
|
||||
if (!ReactFeatureFlags.prepareNewChildrenBeforeUnmountInStack) {
|
||||
ReactReconciler.unmountComponent(
|
||||
prevComponentInstance,
|
||||
safely,
|
||||
false /* skipLifecycle */
|
||||
);
|
||||
}
|
||||
|
||||
var nodeType = ReactNodeTypes.getType(nextRenderedElement);
|
||||
this._renderedNodeType = nodeType;
|
||||
@@ -1132,6 +1136,14 @@ var ReactCompositeComponent = {
|
||||
debugID
|
||||
);
|
||||
|
||||
if (ReactFeatureFlags.prepareNewChildrenBeforeUnmountInStack) {
|
||||
ReactReconciler.unmountComponent(
|
||||
prevComponentInstance,
|
||||
safely,
|
||||
false /* skipLifecycle */
|
||||
);
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
if (debugID !== 0) {
|
||||
var childDebugIDs = child._debugID !== 0 ? [child._debugID] : [];
|
||||
|
||||
@@ -17,6 +17,7 @@ var ReactFeatureFlags = {
|
||||
// render (both initial renders and updates). Useful when looking at prod-mode
|
||||
// timeline profiles in Chrome, for example.
|
||||
logTopLevelRenders: false,
|
||||
prepareNewChildrenBeforeUnmountInStack: true,
|
||||
};
|
||||
|
||||
module.exports = ReactFeatureFlags;
|
||||
|
||||
@@ -200,8 +200,8 @@ describe('ReactTestRenderer', () => {
|
||||
expect(log).toEqual([
|
||||
'render Foo',
|
||||
'mount Foo',
|
||||
'unmount Foo',
|
||||
'render Bar',
|
||||
'unmount Foo',
|
||||
'mount Bar',
|
||||
'unmount Bar',
|
||||
]);
|
||||
|
||||
Reference in New Issue
Block a user