React component not updating when store state has changed

37,140

Solution 1

I had a similar problem and I found out the answer after read this:

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! (https://github.com/reactjs/redux/issues/585)

When I tried use Object.assign({}, object) as you, it didn't work anyway. So was when I found this:

Object.assign only makes shallow copies. (https://scotch.io/bar-talk/copying-objects-in-javascript)

Then I understood that I had to do this: JSON.parse(JSON.stringify(object))

or just this: {...object}

For Arrays: [...theArray]

I hope that this will help you

Solution 2

Because you're not changing the reference, so React's shallow compare doesn't detect the update.

I'm going to use a simple example with blog posts. In your reducer, you're probably doing something as follows:

case FETCH_NEW_POSTS
    let posts = state.posts;
    posts.push(action.payload.posts);
    return {
        ...state, 
        posts
    };

Instead of that, you must do something like the following:

case FETCH_NEW_POSTS
    let posts = [...state.posts]; // we're destructuring `state.posts` inside of array, essentially assigning the elements to a new array.
    posts.push(action.payload.posts);
    return {
        ...state, 
        posts
    };

Depending on your use case Object.assign() or lodash's clone/deepclone may be more idiomatic.

Solution 3

componentWillUpdate receives the incoming props as an argument. At this time, this.props is still the old props. Try changing your method like so:

void componentWillUpdate(nextProps, nextState) {
    L.geoJson(nextProps.data).addTo(this.map);
}
Share:
37,140

Related videos on Youtube

Jacob Mason
Author by

Jacob Mason

Updated on January 06, 2022

Comments

  • Jacob Mason
    Jacob Mason over 2 years

    Below is my component class. The component never seems to execute componentWillUpdate(), even when I can see the state updating by logging before the return in mapStateToProps. The state is 100% changing, however the component doesn't refresh.

    import React, { Component } from 'react'
    import { connect } from 'react-redux'
    import { search } from './mapActions'
    import L from 'leaflet'
    
    
    class Map extends Component {
      componentDidMount() {
        L.Icon.Default.imagePath = './images'
        this.map = new L.Map('map', {
          center: new L.LatLng(this.props.lat, this.props.lng),
          zoom: this.props.zoom,
          layers: L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
            attribution: '<a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          })
        })
      }
      componentWillUpdate() {
        console.log('UPDATE MAP')
        L.geoJson(this.props.data).addTo(this.map)
      }
      render() {
        return <div id="map"></div>
      }
    }
    
    const mapStateToProps = (state) => {
      return {
        isFetching: state.isFetching,
        data: state.data
      }
    }
    
    const mapDispatchToProps = (dispatch) => {
      return {
        search: (name) => {
          dispatch(search(name))
        }
      }
    }
    
    export default connect(
      mapStateToProps,
      mapDispatchToProps
    )(Map)
    

    And here is the map reducer:

    const initialState = {
      isFetching: false,
      data: {}
    }
    
    export const map = (state = initialState, action) => {
      switch(action.type) {
        case 'REQUEST_SEARCH_RESULTS':
          return Object.assign({}, state, {
            isFetching: true
          })
        case 'RECEIVE_SEARCH_RESULTS':
          return Object.assign({}, state, {
            isFetching: false,
            data: action.data
          })
        default:
          return state
      }
    }
    

    After some more testing and logging it seems that when it goes to map state to props the state object it uses to map to props contains the correct data, so state.map.data is correct and I can see the return from the fetch. However when I then log this.props in componentWillUpdate(), the data object is there but empty.

    • Mijamo
      Mijamo about 8 years
      Could you show your reducers as well ? It could be that you mutate the state instead of sending new objects so redux consideres there is no reason to update.
    • Davin Tryon
      Davin Tryon about 8 years
      nothing in the render: <div id="map"></div>
    • Jacob Mason
      Jacob Mason about 8 years
      @DavinTryon I am using Leaflet which handles it's own rendering, however it should still act as if it's updating the component.
    • Jacob Mason
      Jacob Mason about 8 years
      @Mijamo I have added that, thank you.
  • Jacob Mason
    Jacob Mason about 8 years
    Hi Brandon, thank you for the response. The function still doesn't seem to get called. In my three actions, request, receive and fetch, I have logged to console and it all seems to go through fine. I also did some logging in the reducer, and the state definitely changes.
  • Brandon
    Brandon about 8 years
    you'll need to show the code for your actions and the code that is dispatching those actions.
  • Jacob Mason
    Jacob Mason about 8 years
    Hi Brandon, just found something odd that might assist. Check the question for the extra detail.
  • Jacob Mason
    Jacob Mason about 8 years
    Ah, I think I solved it... it seemed the data was there during componentDidUpdate() but not componentWillUpdate(). Isn't that pointless to have the data post-update? Surely you need the new data pre-update?
  • Sean
    Sean about 5 years
    THIS. Thanks, this is exactly the issue, should be the top answer
  • Scruffy
    Scruffy over 4 years
    Just to reword it, in the first case you are pushing to an array, which modifies it, but the array keeps the same reference in memory. So when you return posts, you're returning still the same array, and so the state doesn't update. In the 2nd case you are creating a copy of the array.
  • Akhil Aravind
    Akhil Aravind about 4 years
    This solution should be on top, its the perfect solution
  • Abhishek Bhagate
    Abhishek Bhagate almost 4 years
    This was exactly the issue I was trying to resolve for a day. Helped me a lot ;)
  • dillon.harless
    dillon.harless over 3 years
    I don't see why that changes it... isn't the first code block still returning a new object with a new address in memory? Would love for someone to clarify this for me.
  • Nathanael
    Nathanael over 3 years
    @dillon.harless, no, in JS objects are a reference type. In the first block you're setting posts to the same reference as state.posts. Since React's shallow compare notices the same object pointer, it doesn't get updated. See more here: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/… particularly the "Comparing objects" section at the bottom.
  • David
    David over 3 years
    This answer is still valid Dec 07 2020. spent hours debugging this.
  • John
    John about 3 years
    I tried using Lodash's clone function, but it didn't work. Using {...object} instead however did the trick.
  • MikeyE
    MikeyE about 3 years
    It seems like this might be the answer for my scenario, but where is the connection function imported from?
  • Joe Giusti
    Joe Giusti over 2 years
    var tempArray = imageArrayState tempArray.push(5) setImageArray(tempArray)
  • Nishant Kumar
    Nishant Kumar over 2 years
    This should be the accepted answer
  • snowskeleton
    snowskeleton about 2 years
    {...object} is exactly what I was looking for. Now everything works!