-
Notifications
You must be signed in to change notification settings - Fork 47.2k
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
Bug fix - SetState callback called before component state is updated in ReactShallowRenderer #11507
Conversation
Hmm something is going wrong with the tests, I'm gonna check this. |
|
||
_invokeCallback() { | ||
if (typeof this._callback === 'function' && this._publicInstance) { | ||
this._callback.call(this._publicInstance); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You'll probably want to immediately set it to null before calling. Since now it is outdated.
this._publicInstance = null; | ||
} | ||
|
||
_updateCallback(callback, publicInstance) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe _enqueueCallback
Thanks for the review @gaearon – sending a push with the changes in a bit. BTW, I'm not sure what's going on with the CircleCI tests, all of them seem to be passing by looking at the log. Is this a CI issue, or something I should address? |
I've just pushed the requested changes. The CI tests are stating that I didn't run |
Maybe you didn't run |
Cool, thanks :) Looks like all checks have passed now. |
@@ -183,6 +184,7 @@ class ReactShallowRenderer { | |||
|
|||
if (shouldUpdate) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does ReactDOM call the callback if shouldComponentUpdate
skips an update? I would expect so, but this PR doesn't. Can you verify this?
Maybe it's better to move the callback call here. Then you don't need to duplicate it in two branches.
OK, I've moved the callback call. Regarding the behavior when it('setState callback should be called even if update is skipped', () => {
let stateSuccessfullyUpdated = false;
class Component extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
hasUpdatedState: false,
};
}
shouldComponentUpdate() {
return false;
}
componentWillMount() {
this.setState(
{hasUpdatedState: true},
() => stateSuccessfullyUpdated = this.state.hasUpdatedState,
);
}
render() {
return <div>{this.state.hasUpdatedState}</div>;
}
}
const shallowRenderer = createRenderer();
shallowRenderer.render(<Component />);
expect(stateSuccessfullyUpdated).toBe(true);
}); ... and the callback is indeed being called, but I suspect this isn't enough for the code to even trigger a |
Note I don't know if that's how it works. Can you please check whether ReactDOM calls the callback in this case or not first? |
You could call |
Just wanted to follow up and say I'm working on this today :) |
It seems like ReactDOM does call the |
|
||
componentWillMount() { | ||
setState = (newState, callback) => this.setState(newState, callback); | ||
getState = () => this.state; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These helpers make it a bit hard to read what's really going on in the test. Could you please just store the instance instead, and then read state
and call setState
on it directly?
let setState, getState; | ||
|
||
const div = document.createElement('div'); | ||
document.body.appendChild(div); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to add it to the body for this test.
} | ||
} | ||
|
||
_invokeCallback() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe _invokeCallbackIfNecessary
? It doesn't always exist.
Done! Just pushed the requested changes. |
expect(mockFn).not.toBeCalled(); | ||
|
||
instance.setState({hasUpdatedState: true}, () => { | ||
expect(mockFn).toBeCalled(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't verify that the callback itself has been called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take it from here. Thanks!
Awesome, thanks! :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a few more concerns here.
-
This doesn't seem to handle multiple callbacks correctly. Can this situation happen in practice? I'm not sure. This is how the old Stack reconciler (which is somewhat similar to the shallow renderer) used to handle it: https://github.com/facebook/react/blob/15-stable/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js#L134-L138
-
These lines from the Stack reconciler also surprised me: https://github.com/facebook/react/blob/15-stable/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js#L139-L143. I vaguely remember reading about this issue. Can you research this? Did React not call the
setState
callbacks scheduled fromcomponentWillMount
at all in 15? Did it start to do it in 16? Because if we don't do it in 16 either, we should probably change the shallow renderer to ignore them. Otherwise we should change the ReactDOM test to verify this specific case.
OK, I'll investigate this. I will probably only be able to follow up later this week – is this ok? |
Patch this React bug: - facebook/react#1740 -facebook/react#11507
…in ReactShallowRenderer (facebook#11507) * Create test to verify ReactShallowRenderer bug (facebook#11496) * Fix ReactShallowRenderer callback bug on componentWillMount (facebook#11496) * Improve fnction naming and clean up queued callback before call * Run prettier on ReactShallowRenderer.js * Consolidate callback call on ReactShallowRenderer.js * Ensure callback behavior is similar between ReactDOM and ReactShallowRenderer * Fix Code Review requests (facebook#11507) * Move test to ReactCompositeComponent * Verify the callback gets called * Ensure multiple callbacks are correctly handled on ReactShallowRenderer * Ensure the setState callback is called inside componentWillMount (ReactDOM) * Clear ReactShallowRenderer callback queue before actually calling the callbacks * Add test for multiple callbacks on ReactShallowRenderer * Ensure the ReactShallowRenderer callback queue is cleared after invoking callbacks * Remove references to internal fields on ReactShallowRenderer test
…in ReactShallowRenderer (#11507) * Create test to verify ReactShallowRenderer bug (#11496) * Fix ReactShallowRenderer callback bug on componentWillMount (#11496) * Improve fnction naming and clean up queued callback before call * Run prettier on ReactShallowRenderer.js * Consolidate callback call on ReactShallowRenderer.js * Ensure callback behavior is similar between ReactDOM and ReactShallowRenderer * Fix Code Review requests (#11507) * Move test to ReactCompositeComponent * Verify the callback gets called * Ensure multiple callbacks are correctly handled on ReactShallowRenderer * Ensure the setState callback is called inside componentWillMount (ReactDOM) * Clear ReactShallowRenderer callback queue before actually calling the callbacks * Add test for multiple callbacks on ReactShallowRenderer * Ensure the ReactShallowRenderer callback queue is cleared after invoking callbacks * Remove references to internal fields on ReactShallowRenderer test
This PR aims to fix the bug described in #11496
A test was created to reproduce the error, and to fix it, the
ReactShallowRenderer
callback calling logic was changed, so that it would stop calling the callbacks after theenqueue*
functions, and would start calling them after finishing mounting or updating.Please let me know if there any questions or if anything should be changed / improved :)