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

fix(internalSetState): dont change references inside setState updater #423

Merged

Conversation

drobannx
Copy link
Collaborator

What:
This fixes an issue when using the new React 16.3 React.StrictMode component which invokes setState updater functions twice. This resolves issue #403

Why:
This change is necessary in order to resolve a runtime error when using React.StrictMode. (Note: StrictMode does not affect production code, only development).

How:
This change changes the behavior in the setState updater function by creating new references to state and the stateToSet that is in the closure.

Checklist:

  • Documentation
  • Tests
  • Ready to be merged
  • Added myself to contributors table

I wasn't sure if there were any additional tests that needed to be added. Let me know if there are anything and I can try and add those.

@drobannx drobannx mentioned this pull request Apr 17, 2018
@kentcdodds
Copy link
Member

Thanks for the PR!

This change changes the behavior in the setState updater function by creating new references to state and the stateToSet that is in the closure.

This just changes the references to objects. As far as I know, state is actually never mutated. Unless I'm missing something this change doesn't actually affect anything.

@drobannx
Copy link
Collaborator Author

Hmm, when I was running things locally in StrictMode, these two lines seemed suspect to me:

stateToSet = isStateToSetFunction ? stateToSet(state) : stateToSet
stateToSet = this.props.stateReducer(state, stateToSet)	

The stateToSet reference gets changed to an object with the new state after the first line. Since stateToSet is in the closure and outside this scope, it remains changed on subsequent calls to the updaterFunction.

The first time this is invoked, things work fine. But the second time, before my change, stateToSet was an object and not a function.

Does that make sense?

Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

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

I'm still unclear how these changes are necessary... Each time this function is called it should be called with its own closure. Can you show evidence that these changes fix the problem by creating a test that would fail without them?

@drobannx
Copy link
Collaborator Author

@kentcdodds - I added a failing test case. This test passes with my changes and does not without. The behavior I was seeing in our Application occurred during a call of reset() so I mimicked that in the test.

Hope this helps, let me know if theres anything else I can provide.

@@ -116,6 +116,22 @@ test('can use children instead of render prop', () => {
expect(childrenSpy).toHaveBeenCalledTimes(1)
})

test('should not throw error during strict mode during reset', () => {
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for this! Do you mind showing me this test failing without your changes? I'm really sorry, I'm just struggling to see how your changes really impact anything here :-/

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How would you like me to do that? Would you like me to revert my change in downshift and push? I can do that first thing tomorrow when I get to work.

Copy link
Member

Choose a reason for hiding this comment

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

A screenshot of the failing test would suffice.

Or if you could point me to the part of your code that's making the real impact or explain how this is making a difference. I'm sure I'm missing something, but it looks like this is just renaming variables...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

image

@kentcdodds - Here is a screenshot of the failing test.

Let me try to explain a little more of the issue.

The internalSetState function is not invoked twice by react when in Strict mode. The part that is invoked twice is the updater function that is passed to this.setState. The first time the updater function is invoked, stateToSet is a function (which was passed into internalSetState). Inside the updater function, the stateToSet variable gets reassigned to the result of that function call which is an object (the new state). React then invokes the updater function again (as part of StrictMode). This time, stateToSet is an object since that was changed during the previous invocation.

My change ensures that stateToSet variable never gets reassigned so that it remains the same on subsequent calls of the updater function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here is a CodeSandbox of the issue if this helps:
Edit 8446ynnn7j

@drobannx drobannx mentioned this pull request Apr 18, 2018
4 tasks
Copy link
Member

@kentcdodds kentcdodds left a comment

Choose a reason for hiding this comment

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

I think I understand what's going on now! Thanks for fixing this and for your patience with me!

@kentcdodds kentcdodds merged commit 8105906 into downshift-js:master Apr 18, 2018
@kentcdodds
Copy link
Member

Thanks so much for your help! I've added you as a collaborator on the project. Please make sure that you review the other/MAINTAINING.md and CONTRIBUTING.md files (specifically the bit about the commit messages and the git hooks) and familiarize yourself with the code of conduct (we're using the contributor covenant). You might also want to watch the repo to be notified when someone files an issue/PR. Please continue to make PRs as you feel the need (you can make your branches directly on the repo rather than your fork if you want). Thanks! And welcome to the team :)

@kentcdodds
Copy link
Member

🎉 This PR is included in version 1.31.8 🎉

The release is available on:

Your semantic-release bot 📦🚀

@drobannx
Copy link
Collaborator Author

@kentcdodds - No problem! Happy to contribute. Glad I was able to demonstrate what was going on for you. Some times things are tough to articulate :)

I'll gladly look over those guidelines and contribute where/when I can. This component has been a huge win for us and we would love to give back where we can.

Thanks!

Rendez pushed a commit to Rendez/downshift that referenced this pull request Sep 30, 2018
…downshift-js#423)

* fix(internalSetState): dont change references inside setState updater function

* fix(internalSetState): remove change to arg passed to updater function

* fix(internalSetState): add failing test case
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants