Skip to content

Commit

Permalink
Fixed a batched-state update bug with getDerivedStateFromProps (#12408)
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn authored Mar 21, 2018
1 parent c6b7cea commit dc48326
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 26 deletions.
45 changes: 45 additions & 0 deletions packages/react-dom/src/__tests__/ReactComponentLifeCycle-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -830,4 +830,49 @@ describe('ReactComponentLifeCycle', () => {
'UNSAFE_componentWillUpdate',
]);
});

it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', () => {
const divRef = React.createRef();
let childInstance;

class Child extends React.Component {
state = {local: 0};
static getDerivedStateFromProps(nextProps, prevState) {
return {...prevState, remote: nextProps.remote};
}
updateState = () => {
this.setState(state => ({local: state.local + 1}));
this.props.onChange(this.state.remote + 1);
};
render() {
childInstance = this;
return (
<div onClick={this.updateState} ref={divRef}>{`remote:${
this.state.remote
}, local:${this.state.local}`}</div>
);
}
}

class Parent extends React.Component {
state = {value: 0};
handleChange = value => {
this.setState({value});
};
render() {
return <Child remote={this.state.value} onChange={this.handleChange} />;
}
}

ReactTestUtils.renderIntoDocument(<Parent />);
expect(divRef.current.textContent).toBe('remote:0, local:0');

// Trigger setState() calls
childInstance.updateState();
expect(divRef.current.textContent).toBe('remote:1, local:1');

// Trigger batched setState() calls
ReactTestUtils.Simulate.click(divRef.current);
expect(divRef.current.textContent).toBe('remote:2, local:2');
});
});
1 change: 1 addition & 0 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ export default function<T, P, I, TI, HI, PI, C, CC, CX, PL>(
workInProgress,
value,
props,
workInProgress.memoizedState,
);

if (partialState !== null && partialState !== undefined) {
Expand Down
56 changes: 30 additions & 26 deletions packages/react-reconciler/src/ReactFiberClassComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ export default function(
workInProgress,
instance,
props,
state,
);

if (partialState !== null && partialState !== undefined) {
Expand Down Expand Up @@ -534,7 +535,8 @@ export default function(
function callGetDerivedStateFromProps(
workInProgress: Fiber,
instance: any,
props: any,
nextProps: any,
prevState: any,
) {
const {type} = workInProgress;

Expand Down Expand Up @@ -567,17 +569,13 @@ export default function(
workInProgress.mode & StrictMode)
) {
// Invoke method an extra time to help detect side-effects.
type.getDerivedStateFromProps.call(
null,
props,
workInProgress.memoizedState,
);
type.getDerivedStateFromProps.call(null, nextProps, prevState);
}

const partialState = type.getDerivedStateFromProps.call(
null,
props,
workInProgress.memoizedState,
nextProps,
prevState,
);

if (__DEV__) {
Expand Down Expand Up @@ -698,15 +696,6 @@ export default function(
}
}

let derivedStateFromProps;
if (oldProps !== newProps) {
derivedStateFromProps = callGetDerivedStateFromProps(
workInProgress,
instance,
newProps,
);
}

// Compute the next state using the memoized state and the update queue.
const oldState = workInProgress.memoizedState;
// TODO: Previous state can be null.
Expand Down Expand Up @@ -743,6 +732,18 @@ export default function(
newState = oldState;
}

let derivedStateFromProps;
if (oldProps !== newProps) {
// The prevState parameter should be the partially updated state.
// Otherwise, spreading state in return values could override updates.
derivedStateFromProps = callGetDerivedStateFromProps(
workInProgress,
instance,
newProps,
newState,
);
}

if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
// Render-phase updates (like this) should not be added to the update queue,
// So that multiple render passes do not enqueue multiple updates.
Expand Down Expand Up @@ -867,15 +868,6 @@ export default function(
}
}

let derivedStateFromProps;
if (oldProps !== newProps) {
derivedStateFromProps = callGetDerivedStateFromProps(
workInProgress,
instance,
newProps,
);
}

// Compute the next state using the memoized state and the update queue.
const oldState = workInProgress.memoizedState;
// TODO: Previous state can be null.
Expand Down Expand Up @@ -912,6 +904,18 @@ export default function(
newState = oldState;
}

let derivedStateFromProps;
if (oldProps !== newProps) {
// The prevState parameter should be the partially updated state.
// Otherwise, spreading state in return values could override updates.
derivedStateFromProps = callGetDerivedStateFromProps(
workInProgress,
instance,
newProps,
newState,
);
}

if (derivedStateFromProps !== null && derivedStateFromProps !== undefined) {
// Render-phase updates (like this) should not be added to the update queue,
// So that multiple render passes do not enqueue multiple updates.
Expand Down

0 comments on commit dc48326

Please sign in to comment.