Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Component not updating when redux store modified

I'm using redux thunk to return an API call on an action:

export const getActiveCampaigns = () => {
return (dispatch, getState) => {
    const bearer = 'Bearer ' + getState().login.bearer
    return axios.get(API.path + 'campaign/active/?website_id=' + getState().selectedWebsite.selectedWebsite + '&' + API.beaconAPI_client, { headers: { 'Authorization': bearer } })
    .then(function (response) {
        dispatch({
            type: GET_ACTIVE_CAMPAIGNS,
            activeCampaigns: response.data.response
        })
    })
  }
}

This works as in it successfully returns a list of campaigns, which I'm rendering into another component using:

class ActiveCampaignsDropdown extends Component {
    // usual stuff

    componentDidMount(){
        this.props.dispatch(getActiveCampaigns())
    }

    // render function displays campaigns using this.props.activeCampaigns
}

const mapStateToProps = (state) => {
    return {
        activeCampaigns: state.activeCampaigns.activeCampaigns
    }
}

However, note getState.selectedWebsite.selectedWebsite on the action. This is set from an action elsewhere in the app, where a user chooses a website from a dropdown list. My reducers look like this:

export default function (state = {}, action) {
switch(action.type){
    case SET_SELECTED_WEBSITE:
        return {
            ...state,
            selectedWebsite: action.websiteId
        }
    default:
        return state;
  }
}

export default function (state = {}, action) {
    switch(action.type){
        case GET_ACTIVE_CAMPAIGNS:
        return {
            ...state,
            activeCampaigns: action.activeCampaigns
        }
        default:
        return state;
    }
}

My action for setting the selected website:

export const setSelectedWebsite = (websiteId) => {
    return {
        type: SET_SELECTED_WEBSITE,
        websiteId
    }
}

This is combined with other reducers like so:

export default combineReducers({
    login,
    activeWebsites,
    activeCampaigns,
    selectedWebsite  
})

The problem

The contents of the active campaigns dropdown box works fine on page load - and the state tree does update - but it doesn't update when the selected website changes. From what I can see:

  1. I am dispatching the action correctly
  2. I am updating state, rather than mutating it

I'm quite disappointed that Redux isn't "just working" in this instance, though it is possible I'm overlooking something silly having had only a few hours sleep! Any help appreciated.

like image 978
Matt Saunders Avatar asked Mar 02 '18 07:03

Matt Saunders


1 Answers

In React, components update when one of three things happen:

  • Props changed
  • State changed
  • forceUpdate() is called

In your circumstances, you're looking to update ActiveCampaignsDropdown when state.activeCampaigns changes in the store. To do this, you must hook up your component so that it receives this value as a prop (and thus force an update when it changes).

This can be done as follows:

import {connect} from 'react-redux'

class ActiveCampaignsDropdown extends React.Component { ... }
const mapStateToProps = (state) => ({activeCampaigns: state.activeCampaigns});
const Container = connect(mapStateToProps)(ActiveCampaignsDropdown);

export default Container; 

The final Container component will do all the work of connecting ActiveCampaignsDropdown with the desired store state through its props.

Redux's connect() also allows us to hook up dispatch functions for modifying data in the store. For instance:

// ... component declaration
// ... mapStateToProps

const mapDispatchToProps = (dispatch) => 
{
    return {
        getActiveCampaigns: () => dispatch(getActiveCampaigns())
    };
}

const Container = connect(mapStateToProps, mapDispatchToProps)(ActiveCampaignsDropdown);

Once the mapping functions are defined, the container component is created, and the container is rendered, ActiveCampaignsDropdown will be hooked up correctly. In my example, it will receive 'activeCampaigns' and 'getActiveCampaigns' as props and update accordingly when their values change.


Edit:

After taking another look at your code, I believe your issue is due to the fact that no condition has been met in order to update ActiveCampaignsDropdown when the website has changed. By calling getActiveCampaigns() from your WebsiteDropdown (as per your comment), this is forcing state.activeCampaigns to change, which successfully updates ActiveCampaignsDropdown. As mentioned in one of my comments, 'forcing' this change from a component whose responsibility isn't to do that would be considered bad practice.

A perfectly reasonable solution is for ActiveCampaignsDropdown to 'listen' for changes to the current website and update itself accordingly. For this, you need to do two things:

(1) Map website state to the component

const mapStateToProps = (state) => {
    return {
        activeCampaigns: state.activeCampaigns.activeCampaigns, // unsure why structured like this
        selectedWebsite: state.selectedWebsite.selectedWebsite
    }
}

(2) Move your dispatch call into componentWillReceiveProps

class ActiveCampaignsDropdown extends React.Component
{
    // ...

    componentWillReceiveProps(nextProps)
    {
        if (this.props.selectedWebsite !== nextProps.selectedWebsite)
        {
            this.props.getActiveCampaigns();
        }
    }
}

Now every time the selected website changes, a refresh will occur and componentWillReceiveProps() will be called (causing activeCampaigns to also update). When this update has been applied, another refresh will happen and the rendered dropdown will contain the newly updated campaigns.

Some minor improvements:

  • If a number of your components rely on the state of the current website (which I imagine is many), then you may consider providing them with it via context.

  • Now that your ActiveCampaignsDropdown receives 'selectedWebsite' as a prop, you can pass this directly to your action function instead of having it fetch it from state (using getState()) - which by the way should also be avoided if at all possible.

like image 180
sookie Avatar answered Sep 27 '22 19:09

sookie