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

connect(mapStateToProps) is executed with component props based on the previous state #86

Closed
esamattis opened this issue Sep 2, 2015 · 8 comments

Comments

@esamattis
Copy link
Contributor

Ok, this is really hard to explain so I've set up an example here:

https://github.com/epeli/redux-connect-demo

Build it and play with it with devtools console opened. You should see following errors:

Uncaught TypeError: Cannot read property 'foo' of undefined

This happens because the mapStateToProps of the Item component is called with component props based on the previous store state. The two states are not compatible anymore and it causes all kinds of errors.

I think the parent should render and update the props for the Item component before the mapStateToProps of the Item component can be executed.

@gaearon
Copy link
Contributor

gaearon commented Sep 2, 2015

This happens because the mapStateToProps of the Item component is called with component props based on the previous store state.

Why do you say the previous state? I think they are called with the most recent state—and because the Items have not yet unmounted, they query state that doesn't exist anymore.

I don't think this is something we can fix. React state changes are asynchronous and React may (or may not) batch them. Therefore, the moment you press “Remove”, the Redux store updates, and both Item and App receive the new state. Even if the App state change results in unmounting of Items, that will happen later than mapStateToProps is called for Item.

Unless I'm mistaken, there is nothing we can do. You have two options:

  1. Request all required state at App (or a lower, e.g. ItemList) level and pass it down to “dumb” Items.
  2. Add safeguards to mapStateToProps for “currently unmounting” state. For example, you may return null from render in this case.

Potentially we could have the component generated by connect() return null from its render if mapStateToProps returned null. Does this make any sense? Is this too surprising?

@esamattis
Copy link
Contributor Author

Why do you say the previous state?

Because the store has been updated and the props App has passed to the Item are based on an old store state at the time the store query is run.

Therefore, the moment you press “Remove”, the Redux store updates, and both Item and App receive the new state.

Hmm. Is it actually required for the both to be subscribed to the store? Wouldn't it be enough if only the one most closest to the component root is subscribed? When that component renders it will trigger a render (and a store query) in all child components.

Seems like it would solve this. Although I have no idea how to detect the one closest to the root.

@esamattis
Copy link
Contributor Author

Darn. Just realized it would break apps using pure components. Because the props between the parent and child components does not necessary change it might prevent some children from updating because React thinks there's nothing to the update but there might be because the store has changed.

@esamattis
Copy link
Contributor Author

Potentially we could have the component generated by connect() return null from its render if mapStateToProps returned null. Does this make any sense? Is this too surprising?

I find it really counter intuitive that you have consider state transitions in the store query. This really feels a bug to me but I see how it's unresolvable with the current React APIs.

When thinking about the roots of this issue I think it can be nailed down to the fact that connect can be used in a way to break the unidirectional data flow. It can inject new data to multiple points in the component tree and when you do that you will have a situation where you have some stale data between the tree nodes.

  1. Request all required state at App (or a lower, e.g. ItemList) level and pass it down to “dumb” Items.

This seems to be the right way to do it. Although I sometimes feel that I have to introduce new props to intermediate components just for them to passing those down. Which feels ugly. But I'm not sure it really is because it solves so many issues and weird edge cases like this.

Could Redux just prevent people from ending up with this situation? Maybe by removing ownProps param from mapStateToProps? It took me quite a while to figure out what the hell was going on.

@gaearon
Copy link
Contributor

gaearon commented Sep 2, 2015

Could Redux just prevent people from ending up with this situation? Maybe by removing ownProps param from mapStateToProps? It took me quite a while to figure out what the hell was going on.

We removed it before, and people were unhappy. We do warn in the documentation that we encourage you to follow React flow and avoid connect()ing leaf components.

@esamattis
Copy link
Contributor Author

Heh, before today I would have been one those unhappy people but after pulling my hair out while debugging this today my opinion is certainly different. Sadly I missed that bit of the docs (or didn't take it seriously at the time ).

@esamattis
Copy link
Contributor Author

To whom who are looking for a workaround for this: An interesting observation is that if you do the selection in the component render method the issue is not present at all.

Example:

https://github.com/epeli/redux-connect-demo/pull/1/files

The down side is that your component will do more useless re-renders as you'll have to pass in data that is not actually used for rendering.

esamattis added a commit to esamattis/react-redux that referenced this issue Sep 8, 2015
esamattis added a commit to esamattis/react-redux that referenced this issue Sep 8, 2015
esamattis added a commit to esamattis/react-redux that referenced this issue Sep 9, 2015
esamattis added a commit to esamattis/react-redux that referenced this issue Sep 9, 2015
esamattis added a commit to esamattis/react-redux that referenced this issue Sep 14, 2015
esamattis added a commit to esamattis/react-redux that referenced this issue Sep 14, 2015
esamattis added a commit to esamattis/react-redux that referenced this issue Sep 14, 2015
gaearon pushed a commit that referenced this issue Sep 21, 2015
gaearon pushed a commit that referenced this issue Sep 21, 2015
@gaearon
Copy link
Contributor

gaearon commented Sep 23, 2015

Superseded by #99.

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

No branches or pull requests

2 participants