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

Components not re-rendering with connect() #585

Closed
command-tab opened this issue Aug 19, 2015 · 32 comments
Closed

Components not re-rendering with connect() #585

command-tab opened this issue Aug 19, 2015 · 32 comments
Labels

Comments

@command-tab
Copy link

I'm getting to know Redux by having read the docs and am starting out just modifying the real-world example. In my case, I've swapped the GitHub HTTP API calls for my own, and swapped the Repo and User concepts with analogous ones in my environment. So far, shouldn't be too different, right?

Here's where I'm running into trouble:

My API calls are succeeding, response JSON is getting camelCased and normalized, but my component is not re-rendering after the API request happens successfully. Shortly after USER_REQUEST gets dispatched and the User component updates to show "Loading...", USER_SUCCESS gets dispatched, and its next state does contain a populated entities key:

pasted_image_8_19_15__3_27_pm

That would mean the problem is in the middleware, right? I should be storing the entities from the action in the store, and the component should observe the change and re-render, yes?

Where, in the real-world example, does successfully retrieved and normalized data actually get put into the store? I see where it's being normalized, but not where the result of that normalization is being delivered to the store.

Thanks so much!

@ernieturner
Copy link
Contributor

ernieturner commented Aug 20, 2015

Data gets set/updated/deleted in the store via the results of handling actions in reducers. Reducers receive the current state of a slice of your app, and expect to get new state back. One of the most common reasons that your components might not be re-rendering is that you're modifying the existing state in your reducer instead of returning a new copy of state with the necessary changes (check out the Troubleshooting section). When you mutate the existing state directly, Redux doesn't detect a difference in state and won't notify your components that the store has changed.

So I'd definitely check out your reducers and make sure you're not mutating existing state. Hope that helps!

@andrewchumich
Copy link

In addition to what @ernieturner said, make sure that the mapStateToProps function in your connect(mapStateToProps)(YourConnectedComponent) function maps the relevant state (in this case entities) so that the connected component is listening to that slice of the state.

@timdorr
Copy link
Member

timdorr commented Aug 20, 2015

Also, keep in mind that connect() does a shallow comparison: https://github.com/rackt/react-redux/blob/d5bf492ee35ad1be8ffd5fa6be689cd74df3b41e/src/components/createConnect.js#L91

It's likely that because the shape of your connected state didn't change, it's assuming to not do an update.

@command-tab
Copy link
Author

Ah, got it! My mapStateToProps was attempting to extract an object from state, but wasn't specifying the correct key. Instead of extracting entities.users[login], I had it set to entities.users[id]. The entities.users object was keyed by login string, so when the numeric id wasn't found, no props actually changed, so no re-render was needed.

Thanks, all, for the help! I really appreciate the time you took to assist.

@wzup
Copy link

wzup commented Aug 21, 2016

@timdorr
That's ridiculous. How to force connect() check deep changes? Otherwise one has to create dozens of container components for every deeper level of state. It is not normal

@timdorr
Copy link
Member

timdorr commented Aug 21, 2016

You absolutely should have many connected components in any reasonably-sized app. If you have one single top level connected component, you are going to be re-rendering large parts of your app unnecessarily every time state changes at all. And you'll be obviating one of the primary optimizations of react-redux, which is to only re-render when the state it is subscribed to changes.

If you need to evaluate a complex query of state, then use reselect to speed that up.

@naw
Copy link
Contributor

naw commented Aug 22, 2016

@timdorr @wzup

It may be helpful to clarify that connecting at lower places in the tree is not strictly necessary to solve the deep comparison issue.

Connecting at lower places in your component tree may indeed help with performance, but it is not the fundamental solution to the deep comparison problem. The solution to deep comparison problems is for your reducers to update state immutably, so that a shallow comparison is adequate for knowing when a deeper piece of state changed.

In other words, connecting a top-level component to a large nested piece of state works just fine even with shallow comparisons, as long as your reducers return new state without mutating existing state objects.

@Zacqary
Copy link

Zacqary commented Sep 1, 2016

@naw Thanks for that tip. Based on it I ended up adding a new reducer where I was experiencing this problem:

const reducers = combineReducers({
    //...bunch of reducers which always return objects of 3 or 4 properties that change sometimes but
    // the shape stays the same
    dataVersion: (state = Symbol(), action = {}) => {
        switch (action.type) {
            case SAME_ACTION_AS_THE_ONE_THAT_UPDATES_A_COMPLEX_PROPERTY:
            case ANOTHER_ACTION_THAT_ALSO_UPDATES_A_COMPLEX_PROPERTY:
                return Symbol():
            default:
                return state;
        }
    }
})

This still feels weird to me, though. If I've got a reducer that goes:

    switch (action.type) {
       case UPDATE_THIS:
           return {a: action.a, b: action.b, c: action.c};
       default:
           return state;
     }

the fact that I'm returning a newly initialized object that just happens to be the same shape as the previous state should be enough to trigger a previousState !== newState. If I'm supposed to be returning immutable data each time, it seems like it's more inefficient to be doing a shallow comparison of the shape rather than just the identity. (Assuming that's what Redux is actually doing, that is. The fact that my Symbol workaround works makes me think that's what's going on)

In this particular case adding more container components doesn't solve the issue. Sadly I can't link to a whole github project for clarification because this is my company's closed source stuff I'm dealing with here.

@jimbolla
Copy link
Contributor

jimbolla commented Sep 1, 2016

@Zacqary The comparison of current state to previous state is done with strict equal (===). The comparison of stateProps (the result of mapStateToProps) is done with shallow equal.

@naw
Copy link
Contributor

naw commented Sep 1, 2016

@Zacqary

mapStateToProps is "assembling" data for your connected component by reaching into the store.

mapStateToProps pulls various piece(s) out of the store, and assigns them to keys in a new stateProps object.

The resulting statePropsobject will be new every time (so its identity changes), so we can't just compare the object identity of the stateProps object itself --- we have to go down one level and check the object identity of each value, which is where shallowEqual comes into play, as @jimbolla said.

Typically problems arise when you write reducers in such a way that the object identity of a piece of state doesn't change, while some nested attribute within it does change. This is mutating state rather than returning new state immutably, which causes react-redux to think nothing changed, when in fact, something did change.

the fact that I'm returning a newly initialized object that just happens to be the same shape as the previous state should be enough to trigger a previousState !== newState.

If you return a new object for some slice of state, and mapStateToProps uses that slice of state as the value for one of its keys, then react-redux will see that the object identity changed, and will not prevent a re-render.

Is there something specific that isn't working as you expect it to? I'm not really sure what you mean by your Symbol workaround.

@markau
Copy link

markau commented Nov 3, 2016

It's worth considering whether the additional component should simply be moved down into the connected() component to inherit the props. I got to this issue by attempting to connect() two components, but that was unnecessary complexity.

@talitore
Copy link

talitore commented Nov 4, 2016

@ernieturner I know this is old but you might want to update your Troubleshooting Section link.

Cheers

@AlexanderKozhevin
Copy link

AlexanderKozhevin commented Jan 11, 2017

I had this problem, well not exactly.
I applied the properties to state, like:

  constructor(props){
    this.state = {
      text: props.text
    }
  }

And it wasn't working. So I applied value directly from props like

{this.props.text}

And it works just fine :)

@cdubois-mh
Copy link

@AlexanderKozhevin Unless I am mistaken, your constructor is missing a super(props).
Normally you're not even allowed to use this before you call super(props).

@AlexanderKozhevin
Copy link

@cdubois-mh Right you are, thanx for note.

@daedalius
Copy link

daedalius commented Jul 17, 2017

I've written custom-made app root reducer and get that problem.
Returning a copy of whole state
{...state}
in the end of the root reducer helped for me

@CedSharp
Copy link

@daedalius That's how a reducer is supposed to work.
It should return the state if the action is irrelevant, or return a copy of the state with the expected modified values.

If you don't return a copy, then react won't always be able to detect that a value changed.
( for example, if you modify a nested entry )

Check this state:

{
    "clients": [
        { "name": "John", "cid": 4578 },
        { "name": "Alex", "cid": 5492 },
        { "name": "Bob",  "cid": 254 }
    ]
}

If you modify the name of a client, but don't return a clone of the state, then you didn't modify the state, you modified the object in the array.

The object itself will still have the same reference as before, so react will miss the change.
By returning a clone, the state itself's reference will now be different ( since it's a new object ) and react will initiate the whole re-rendering process.

If I am wrong, please correct me, but this is how I understand reducers to work.

@markerikson
Copy link
Contributor

Yep, that's correct. Also, @daedalius , note that you don't want to always make a shallow copy of state - you should only do that if something changed, otherwise you may wind up with unnecessary re-rendering of your components.

@BrookShuihuaLee
Copy link

force update

<AfterConnect
    _forceUpdate={Symbol()}
/>

@CedSharp
Copy link

@BrookShuihuaLee What is that supposed to mean? you didn't provide any information or explanation of your piece of code. What does it do? Why is it relevant to the current discussion at hand?

@BrookShuihuaLee
Copy link

@CedSharp The shallowEqual function will return false, if we set _forceUpdate={Symbol()}

@CedSharp
Copy link

@BrookShuihuaLee Would be great if you could include an explanation like this in your first answer, this way people will understand it better ^^

@BrookShuihuaLee
Copy link

@CedSharp yeah

@markerikson
Copy link
Contributor

@BrookShuihuaLee : That seems like a bad idea. Why would you want to do that?

@CedSharp
Copy link

@BrookShuihuaLee , I agree with @markerikson .
There is a reason why you're supposed to NOT return a clone when the state didn't change. Unless you can provide a valid example in which your solution seems to be necessary, I would strongly recommend against going around how Redux works.

@BrookShuihuaLee
Copy link

@CedSharp
@markerikson
Because the component AfterConnect will be rerendered frequenctly, by design. It's ok to rerender it once more when its parent component is being rerendered. I don't want to do all computation in the parent component and set tons of props for the AfterConnect.

@CedSharp
Copy link

@BrookShuihuaLee, I'm sorry, but how is your component relevant to the discussion at hand?
If I understand properly, you are talking about your own custom component? ForceUpdate is something very strongly discouraged in the React world and i wouldn't recommend it as a solution.

Instead, you could use an observable solution where you look for changes somewhere else than the state? I don't know why you'd need to re-render that specific component without it's state being modified, but if you know why it should re-render, maybe you could use something like mobx?
(https://github.com/mobxjs/mobx)

I'm trying to understand what your solution is, and why it applies in this situation.

@BrookShuihuaLee
Copy link

@CedSharp I don't mean that everyone should use forceUpdate. But the recommendation is one thing, the choice is another. React also keep the ReactComponent.prototype.forceUpdate for developers. People need choices.

I was trying to give another choice. Maybe, it's not the best choice.

@pthieu
Copy link

pthieu commented Sep 7, 2017

Not optimal, but I've run into this problem because I've structured my app state incorrectly. I got around the shallow comparison issue by just updating a shallow field on the the slice of the state that was causing me trouble.

case SOME_ACTION:
      return {
        ...state,
        ts: (new Date).getTime(),
      };

IMO, not a good solution, but for anyone currently in a bind (i.e. you have to release end of day), it's a quick fix and you don't have to force a render somewhere else.

The code should be refactored so that a shallow comparison, even on a deeply nested state will work.

@joeychia
Copy link

The state must be immutable. Deep copy the state in reducer, then shallow comparison in connect is enough and effective.

@robhadfield
Copy link

robhadfield commented Oct 25, 2017

My issue was having an object with arrays within it. As it was shallow comparison it never reached the objects within the child arrays. I solved it by using list:state.obj.list rather than lists:state.obj in mapStateToProps 👍🏼

estebano added a commit to estebano/stepin-app that referenced this issue Nov 1, 2017
@amackintosh
Copy link

amackintosh commented Nov 18, 2017

I just encountered this issue, and after reading the first couple replies, I checked my reducer and noticed I had a typo in there. I renamed a property and updated the INITIAL_STATE but I forgot to change the key that was being updated by this problematic action.

I was basically updating some random key that wasn't used in the component, so obviously, it wouldn't re-render when this random key was updated.

CHECK FOR TYPOS IN YOUR REDUCER! :)

@reduxjs reduxjs deleted a comment from Herdismaria Jan 20, 2018
@reduxjs reduxjs locked as resolved and limited conversation to collaborators Jan 20, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests