Search code examples
reactjsreact-nativereduxreact-redux

React native redux not updating component


I'm trying to setup redux with react native but it is not updating my components when the store updates.

class Dinner extends React.Component {

    componentDidUpdate() {
        console.log('does not get called when store update');
    }

    setSelected = (meal) => {
        var index = this.props.selectedDinner.indexOf(meal);

        if (index === -1) {
            console.log('Adding meal to selected: '+meal.name);
            if (this.props.selectedDinner[0] === null) {
                var tempArr = this.props.selectedDinner;
                tempArr[0] = meal;
                this.props.setSelectedDinner(tempArr);

            } else if(this.props.selectedDinner[1] === null)  {
                var tempArr = this.props.selectedDinner;
                tempArr[1] = meal;
                this.props.setSelectedDinner(tempArr);
            } else if(this.props.selectedDinner[2] === null)  {
                var tempArr = this.props.selectedDinner;
                tempArr[2] = meal;
                this.props.setSelectedDinner(tempArr);
            }
        } else {
            console.log("removing meal from selected: "+meal.name)
            var tempArr = this.props.selectedDinner;
            tempArr[index] = null;
            this.props.setSelectedDinner(tempArr);
        }
        LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
        this.forceUpdate()
    };

    render() {
        return (
          <View style={{flex: 1, width: 360, justifyContent: 'center', alignItems: 'center', paddingBottom: 20}}>
            <View style={{width: 340, backgroundColor: 'white', justifyContent: 'flex-start', alignItems: 'center'}}>
              <Text style={{height: 50, fontSize: 20, fontWeight: 'bold', flex: 1, justifyContent: 'center', alignItems: 'center'}}>Dinner</Text>
              {
                this.props.dinnerFeed.map((prop, key) => 
                  prop === null ?
                    <TouchableOpacity style={{width: 320, height: 120, backgroundColor: 'lightgrey', flex: 1, justifyContent: 'center', alignItems: 'center', zIndex: 1, marginBottom: 10, flexShrink: 0}} key={key}><LoadingMealTile /></TouchableOpacity>
                    :
                    (prop === 'none' ? 
                      <TouchableOpacity style={{width: 320, height: 120, backgroundColor: 'lightgrey', flex: 1, justifyContent: 'center', alignItems: 'center', zIndex: 1, marginBottom: 10, flexShrink: 0}} key={key}><BlankMealTile /></TouchableOpacity>
                      :
                      this.props.selectedDinner === null || this.props.selectedDinner.indexOf(prop) === null ?
                        <TouchableOpacity onPress={this.setSelected.bind(this, prop)} style={{width: 320, height: 120, backgroundColor: 'lightgrey', flex: 1, justifyContent: 'center', alignItems: 'center', zIndex: 1, marginBottom: 10, flexShrink: 0}} key={key}><MealTile selected={-1} name={prop.name} id={prop.id} url={prop.url} key={key}/></TouchableOpacity>
                        :
                        <TouchableOpacity onPress={this.setSelected.bind(this, prop)} style={{width: 320, height: 120, backgroundColor: 'lightgrey', flex: 1, justifyContent: 'center', alignItems: 'center', zIndex: 1, marginBottom: 10, flexShrink: 0}} key={key}><MealTile selected={this.props.selectedDinner.indexOf(prop)} name={prop.name} id={prop.id} url={prop.url} key={key}/></TouchableOpacity>
                    )
                )  
              }

              <TouchableOpacity onPress={this.props.loadFeedMeals} style={{width: 320, height: 50, backgroundColor: 'lightgrey', flex: 1, justifyContent: 'center', alignItems: 'center', zIndex: 1, marginBottom: 10}}><Text style={{fontSize: 15, }}>Load More Meals</Text></TouchableOpacity>
            </View>
          </View>
        );
      }
    }


    function mapStateToProps(state) { 
      return {
           dinnerFeed: state.dinnerFeed,
           selectedDinner: state.selectedDinner,
      }
    };

    function mapDispatchToProps(dispatch) {
      return {
        setDinnerMeals: (dinnerMeals) => dispatch(setDinnerMeals(dinnerMeals)),
        setSelectedDinner: (selectedDinner) => dispatch(setSelectedDinner(selectedDinner)),

      }
    };

    export default connect(mapStateToProps, mapDispatchToProps)(Dinner);

The function setSelectedDinner correctly changes the redux store, but the component doesn't call its componentDidUpdate function

edit:Here is the reducer code


    export default (state, action) => {
        console.log(action);
        switch (action.type) {
            case "SET-SELECTED-DINNER":
                return {
                        ...state,
                        selectedDinner: action.selectedDinner
                  };
            default:
                return state;
        }
    };

I believe this code shouldn't mutate the state directly because i have used this reducer for redux on a reactjs project


Solution

  • The most common cause of Redux state being updated, but of connected components not updating is mutating state within the reducer. A very similar common problem is not recognising that the Redux connect HoC performs a shallow comparison.

    This is because Redux checks for changes by using object equality. (if a === comparison returns true, the object is considers to have not changed.)

    Consider the following incorrect reducer:

    function todoApp(state = initialState, action) {
      switch (action.type) {
        case SET_VISIBILITY_FILTER:
          return state.visibilityFilter = action.filter;
        default:
          return state
      }
    } 
    

    Since the reducer above mutated the state, no updated is triggered.

    A correct example (taken from the Redux documentation) is shown below:

    function todoApp(state = initialState, action) {
      switch (action.type) {
        case SET_VISIBILITY_FILTER:
          return Object.assign({}, state, {
            visibilityFilter: action.filter
          })
        default:
          return state
      }
    } 
    

    Shallow comparison

    As well as the above best practice of not mutating state but always returning a new state for anything that changes, it is important to remember that Redux connect is using this shallow comparison for all objects returned by mapStateToProps.

    Consider a simplified version of your mapStateToProps function:

    function mapStateToProps(state) { 
        return {
            selectedDinner: state.selectedDinner,
        }
    };
    

    And now consider how you are passing selected dinner to your setSelectedDinner action (again simplified):

    setSelected = (meal) => {
        var index = this.props.selectedDinner.indexOf(meal);
    
        // when index === -1 we need to add this meal
        if (index === -1) {     
            // there is no meal at index 0 so we add it                    
            if (this.props.selectedDinner[0] === null) {
                // NOTICE - the line below still references selected dinner in state!
                var tempArr = this.props.selectedDinner;
                tempArr[0] = meal;
    
                // NOTICE - we are calling setSelectedDinner with the same object
                // that comes from state! 
                this.props.setSelectedDinner(tempArr);
            }
        }
    }
    

    So the problem is, that in your reducer function you are replacing the selectedDinner object with itself, so Redux sees no updates.

    The quickest change that will fix your problem is to modify the reducer function to read (the slice call clones your array):

    export default (state, action) => {
        switch (action.type) {
            case "SET-SELECTED-DINNER":
                return {
                        ...state,
                        selectedDinner: action.selectedDinner.slice()
                  };
            default:
                return state;
        }
    };
    

    There are lots of other small changes that would make this code easier to work with and less prone to such bugs. Two simple ones are:

    1. Move the logic that modifies your selectedDinner array out of the component, and place it in the reducer.

    2. Introduce selectors