Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't recreate instance when resuming a class component's initial mount #9608

Merged
merged 2 commits into from
May 5, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/fiber/tests-passing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ src/renderers/__tests__/ReactCompositeComponentState-test.js
* should update state when called from child cWRP
* should merge state when sCU returns false
* should treat assigning to this.state inside cWRP as a replaceState, with a warning
* should treat assigning to this.state inside cWM as a replaceState, with a warning

src/renderers/__tests__/ReactEmptyComponent-test.js
* should not produce child DOM nodes for null and false
Expand Down Expand Up @@ -1675,6 +1676,7 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
* can resume work in a subtree even when a parent bails out
* can resume work in a bailed subtree within one pass
* can resume mounting a class component
* reuses the same instance when resuming a class instance
* can reuse work done after being preempted
* can reuse work that began but did not complete, after being preempted
* can reuse work if shouldComponentUpdate is false, after being preempted
Expand Down
40 changes: 40 additions & 0 deletions src/renderers/__tests__/ReactCompositeComponentState-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,44 @@ describe('ReactCompositeComponent-state', () => {
'Use setState instead.',
);
});

it('should treat assigning to this.state inside cWM as a replaceState, with a warning', () => {
spyOn(console, 'error');

let ops = [];
class Test extends React.Component {
state = {step: 1, extra: true};
componentWillMount() {
this.setState({step: 2}, () => {
// Tests that earlier setState callbacks are not dropped
ops.push(
`callback -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
);
});
// Treat like replaceState
this.state = {step: 3};
}
render() {
ops.push(
`render -- step: ${this.state.step}, extra: ${!!this.state.extra}`,
);
return null;
}
}

// Mount
const container = document.createElement('div');
ReactDOM.render(<Test />, container);

expect(ops).toEqual([
'render -- step: 3, extra: false',
'callback -- step: 3, extra: false',
]);
expect(console.error.calls.count()).toEqual(1);
expect(console.error.calls.argsFor(0)[0]).toEqual(
'Warning: Test.componentWillMount(): Assigning directly to ' +
"this.state is deprecated (except inside a component's constructor). " +
'Use setState instead.',
);
});
});
171 changes: 114 additions & 57 deletions src/renderers/shared/fiber/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,59 @@ module.exports = function(
return instance;
}

function callComponentWillMount(workInProgress, instance) {
if (__DEV__) {
startPhaseTimer(workInProgress, 'componentWillMount');
}
const oldState = instance.state;
instance.componentWillMount();
if (__DEV__) {
stopPhaseTimer();
}

if (oldState !== instance.state) {
if (__DEV__) {
warning(
false,
'%s.componentWillMount(): Assigning directly to this.state is ' +
"deprecated (except inside a component's " +
'constructor). Use setState instead.',
getComponentName(workInProgress),
);
}
updater.enqueueReplaceState(instance, instance.state, null);
}
}

function callComponentWillReceiveProps(
workInProgress,
instance,
newProps,
newContext,
) {
if (__DEV__) {
startPhaseTimer(workInProgress, 'componentWillReceiveProps');
}
const oldState = instance.state;
instance.componentWillReceiveProps(newProps, newContext);
if (__DEV__) {
stopPhaseTimer();
}

if (instance.state !== oldState) {
if (__DEV__) {
warning(
false,
'%s.componentWillReceiveProps(): Assigning directly to ' +
"this.state is deprecated (except inside a component's " +
'constructor). Use setState instead.',
getComponentName(workInProgress),
);
}
updater.enqueueReplaceState(instance, instance.state, null);
}
}

// Invokes the mount life-cycles on a previously never rendered instance.
function mountClassInstance(
workInProgress: Fiber,
Expand Down Expand Up @@ -339,13 +392,7 @@ module.exports = function(
}

if (typeof instance.componentWillMount === 'function') {
if (__DEV__) {
startPhaseTimer(workInProgress, 'componentWillMount');
}
instance.componentWillMount();
if (__DEV__) {
stopPhaseTimer();
}
callComponentWillMount(workInProgress, instance);
// If we had additional state updates during this life-cycle, let's
// process them now.
const updateQueue = workInProgress.updateQueue;
Expand Down Expand Up @@ -389,6 +436,34 @@ module.exports = function(
const newUnmaskedContext = getUnmaskedContext(workInProgress);
const newContext = getMaskedContext(workInProgress, newUnmaskedContext);

const oldContext = instance.context;
const oldProps = workInProgress.memoizedProps;

if (
typeof instance.componentWillReceiveProps === 'function' &&
(oldProps !== newProps || oldContext !== newContext)
) {
callComponentWillReceiveProps(
workInProgress,
instance,
newProps,
newContext,
);
}

// Process the update queue before calling shouldComponentUpdate
const updateQueue = workInProgress.updateQueue;
if (updateQueue !== null) {
newState = beginUpdateQueue(
workInProgress,
updateQueue,
instance,
newState,
newProps,
priorityLevel,
);
}

// TODO: Should we deal with a setState that happened after the last
// componentWillMount and before this componentWillMount? Probably
// unsupported anyway.
Expand All @@ -411,39 +486,34 @@ module.exports = function(
return false;
}

// If we didn't bail out we need to construct a new instance. We don't
// want to reuse one that failed to fully mount.
const newInstance = constructClassInstance(workInProgress, newProps);
newInstance.props = newProps;
newInstance.state = newState = newInstance.state || null;
newInstance.context = newContext;
// Update the input pointers now so that they are correct when we call
// componentWillMount
instance.props = newProps;
instance.state = newState;
instance.context = newContext;

if (typeof newInstance.componentWillMount === 'function') {
if (__DEV__) {
startPhaseTimer(workInProgress, 'componentWillMount');
}
newInstance.componentWillMount();
if (__DEV__) {
stopPhaseTimer();
if (typeof instance.componentWillMount === 'function') {
callComponentWillMount(workInProgress, instance);
// componentWillMount may have called setState. Process the update queue.
const newUpdateQueue = workInProgress.updateQueue;
if (newUpdateQueue !== null) {
newState = beginUpdateQueue(
workInProgress,
updateQueue,
instance,
newState,
newProps,
priorityLevel,
);
}
}
// If we had additional state updates, process them now.
// They may be from componentWillMount() or from error boundary's setState()
// during initial mounting.
const newUpdateQueue = workInProgress.updateQueue;
if (newUpdateQueue !== null) {
newInstance.state = beginUpdateQueue(
workInProgress,
newUpdateQueue,
newInstance,
newState,
newProps,
priorityLevel,
);
}

if (typeof instance.componentDidMount === 'function') {
workInProgress.effectTag |= Update;
}

instance.state = newState;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I do componentWillMount() { this.state = {}; } What should it do?


return true;
}

Expand Down Expand Up @@ -476,29 +546,16 @@ module.exports = function(
// ever the previously attempted to render - not the "current". However,
// during componentDidUpdate we pass the "current" props.

if (oldProps !== newProps || oldContext !== newContext) {
if (typeof instance.componentWillReceiveProps === 'function') {
if (__DEV__) {
startPhaseTimer(workInProgress, 'componentWillReceiveProps');
}
instance.componentWillReceiveProps(newProps, newContext);
if (__DEV__) {
stopPhaseTimer();
}

if (instance.state !== workInProgress.memoizedState) {
if (__DEV__) {
warning(
false,
'%s.componentWillReceiveProps(): Assigning directly to ' +
"this.state is deprecated (except inside a component's " +
'constructor). Use setState instead.',
getComponentName(workInProgress),
);
}
updater.enqueueReplaceState(instance, instance.state, null);
}
}
if (
typeof instance.componentWillReceiveProps === 'function' &&
(oldProps !== newProps || oldContext !== newContext)
) {
callComponentWillReceiveProps(
workInProgress,
instance,
newProps,
newContext,
);
}

// Compute the next state using the memoized state and the update queue.
Expand Down
82 changes: 79 additions & 3 deletions src/renderers/shared/fiber/__tests__/ReactIncremental-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,76 @@ describe('ReactIncremental', () => {

ops = [];
ReactNoop.flush();
expect(ops).toEqual(['Foo constructor: foo', 'Foo', 'Bar']);
expect(ops).toEqual(['Foo', 'Bar']);
});

it('reuses the same instance when resuming a class instance', () => {
let ops = [];
let foo;
class Parent extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return <Foo prop={this.props.prop} />;
}
}

let constructorCount = 0;
class Foo extends React.Component {
constructor(props) {
super(props);
// Test based on a www bug where props was null on resume
ops.push('constructor: ' + props.prop);
constructorCount++;
}
componentWillMount() {
ops.push('componentWillMount: ' + this.props.prop);
}
componentWillReceiveProps() {
ops.push('componentWillReceiveProps: ' + this.props.prop);
}
componentDidMount() {
ops.push('componentDidMount: ' + this.props.prop);
}
componentWillUpdate() {
ops.push('componentWillUpdate: ' + this.props.prop);
}
componentDidUpdate() {
ops.push('componentDidUpdate: ' + this.props.prop);
}
render() {
foo = this;
ops.push('render: ' + this.props.prop);
return <Bar />;
}
}

function Bar() {
ops.push('Foo did complete');
return <div />;
}

ReactNoop.render(<Parent prop="foo" />);
ReactNoop.flushDeferredPri(25);
expect(ops).toEqual([
'constructor: foo',
'componentWillMount: foo',
'render: foo',
'Foo did complete',
]);

foo.setState({value: 'bar'});

ops = [];
ReactNoop.flush();
expect(constructorCount).toEqual(1);
expect(ops).toEqual([
'componentWillMount: foo',
'render: foo',
'Foo did complete',
'componentDidMount: foo',
]);
});

it('can reuse work done after being preempted', () => {
Expand Down Expand Up @@ -1030,7 +1099,7 @@ describe('ReactIncremental', () => {
expect(ops).toEqual(['Foo', 'Bar:B2', 'Bar:D']);

// We expect each rerender to correspond to a new instance.
expect(instances.size).toBe(5);
expect(instances.size).toBe(4);
});

it('gets new props when setting state on a partly updated component', () => {
Expand Down Expand Up @@ -1094,6 +1163,12 @@ describe('ReactIncremental', () => {

class LifeCycle extends React.Component {
state = {x: this.props.x};
componentWillReceiveProps(nextProps) {
ops.push(
'componentWillReceiveProps:' + this.state.x + '-' + nextProps.x,
);
this.setState({x: nextProps.x});
}
componentWillMount() {
ops.push('componentWillMount:' + this.state.x + '-' + this.props.x);
}
Expand Down Expand Up @@ -1132,6 +1207,7 @@ describe('ReactIncremental', () => {

expect(ops).toEqual([
'App',
'componentWillReceiveProps:0-1',
'componentWillMount:1-1',
'Trail',
'componentDidMount:1-1',
Expand Down Expand Up @@ -1179,7 +1255,7 @@ describe('ReactIncremental', () => {

expect(ops).toEqual([
'App',
'componentWillMount:1(ctor)',
'componentWillMount:0(willMount)',
'render:1(willMount)',
'componentDidMount:1(willMount)',
]);
Expand Down