9

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.

Matt Saunders
  • 4,073
  • 7
  • 33
  • 47
  • is ur state tree changing ? and it will be better if you can post the component where you are using this state object as well. – semuzaboi Mar 02 '18 at 07:39
  • Please show us your component implementation and the part where you connect to the store. – Tobias Golbs Mar 02 '18 at 07:40
  • it's connected by calling props.dispatch and mapStateToProps. The state tree is changing, but only on page load. – Matt Saunders Mar 02 '18 at 07:45
  • Show me the `GET_ACTIVE_CAMPAIGNS` reducer – Medet Tleukabiluly Mar 02 '18 at 07:48
  • EDIT: added to the code above – Matt Saunders Mar 02 '18 at 07:49
  • You are not saving the `activeCampaigns`, it should be stored, as state changes, your `activeCampaigns` is always empty – Medet Tleukabiluly Mar 02 '18 at 07:51
  • @MedetTleukabiluly how do you mean? – Matt Saunders Mar 02 '18 at 07:53
  • How sure are you that your state has always a `state.activeCampaigns.activeCampaigns` property? Your default state seems to be an empty object – Icepickle Mar 02 '18 at 07:55
  • @Icepickle certain. I am saving changes in localStorage, so when I reload the page, the campaigns change to match the website Id they were given. The problem is the campaigns don't change immediately. – Matt Saunders Mar 02 '18 at 07:57
  • Show the `SET_SELECTED_WEBSITE` Action – Medet Tleukabiluly Mar 02 '18 at 07:58
  • It's reducer you added, and its duplicated, show the Action – Medet Tleukabiluly Mar 02 '18 at 08:00
  • make sure `setSelectedWebsite(websiteId)` always has value, console.log it, probably its not getting value – Medet Tleukabiluly Mar 02 '18 at 08:04
  • If I console log it in the action, it gets the websiteId every time... – Matt Saunders Mar 02 '18 at 08:05
  • Is it because the getActiveCampaigns action is only called on component mounting, and when setSelectedWebsite is called, that is performed away from the campaigns component? If this is the case, I've completely misunderstood what Redux is supposed to do! I assumed that *any change of state* would be reflected, automatically, across the application where appropriate. – Matt Saunders Mar 02 '18 at 08:09
  • Do you connect your component to the store via redux's `connect()` method? – sookie Mar 02 '18 at 08:12
  • Yes. If I am not mistaken, this does that: this.props.dispatch(setSelectedWebsite(e.target.value)) – Matt Saunders Mar 02 '18 at 08:19
  • check if this is `state.activeCampaigns.activeCampaigns` or just `state.activeCampaigns` – semuzaboi Mar 02 '18 at 08:22
  • The `connect()` method takes two functions: 'mapStateToProps' and 'mapDispatchToProps.' `connect()` returns a stateless container component whose only function is to provide your 'ActiveCampaignsDropdownComponent' with the desired props from store. The code to do this is as follows: `const ActiveCampaignsDropdownContainer = connect(mapStateToProps, mapDispatchToProps)(ActiveCampaignsDropdownComponent);`. It is this container you need to render. – sookie Mar 02 '18 at 08:27
  • See https://stackoverflow.com/questions/41021369/how-to-use-connect-from-react-redux for more details – sookie Mar 02 '18 at 08:31
  • @sookie I think I follow but please could you provide an answer with a code sample that I can try? – Matt Saunders Mar 02 '18 at 10:07
  • @MattSaunderts Sure, give me a few minutes – sookie Mar 02 '18 at 10:57
  • @sookie I may have resolved this, but it feels a little hacky. In my website dropdown I am calling setSelectedWebsite(website_id). I have discovered that if I also call getActiveCampaigns - this.props.dispatch(getActiveCampaigns()) - here, the campaigns dropdown reloads as it's supposed to. This feels like an anti-pattern - calling an action on an event handler in a different component in order to "force" a state refresh. Thoughts? – Matt Saunders Mar 02 '18 at 11:15
  • 1
    @MattSaunders Yes, forcing a refresh from another component whose responsibility isn't to do that would be bad design. After looking at your code again, I think I know what your problem is. When you switch website, your ActiveCampaignsDropdown doesn't know about it. Because none of its props have changed, there is no update. I'll edit my answer to include a full solution for you. PS: It would also be considered bad practice to provide a component with the `dispatch` function directly. Instead use mapDispatchToProps to provide wrapper functions around your dispatch calls (see my answer below) – sookie Mar 03 '18 at 00:53

1 Answers1

9

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.

sookie
  • 2,437
  • 3
  • 29
  • 48
  • Thanks very much for this. Now if I'm using mapDispatchToProps for all action calls, how can I pass parameters? For example, if this call is in mapDispatchToProps, how would I pass an argument into it: setSelectedCampaign: () => dispatch(setSelectedCampaign()). Hopefully, not using internal state! – Matt Saunders Mar 04 '18 at 18:27
  • I've resolved this by passing in the value like so: setSelectedCampaign: (campaignId) => dispatch(setSelectedCampaign(campaignId)) – Matt Saunders Mar 04 '18 at 20:14
  • @MattSaunders Yep, you got it – sookie Mar 05 '18 at 14:38