Clev3r
Clev3r

Reputation: 1578

Redux dispatch actions in response to actions

I'm thinking through some scenarios surrounding redux and I wasn't able to find a clean solution to this example:

Let's say you have a component that is a list of recipes. When you select a recipe from that list, you dispatch(RECIPE_SELECTED). That async action-creator may do additional related to the recipe - perhaps go async fetch the ingredients for the recipes, save the selection to the server, whatever.

In a totally separate component you have a list of professional chefs. The desired behavior is that when a user selects a recipe, you populate the list of professional chefs with any chef who has a variation of the selected recipe.

How do you listen for RECIPE_SELECTED and then dispatch a totally unrelated action that has a dependency on the recipe? Something like...

when RECIPE_SELECTED:recipe
  loadChefs(recipe).then(res => dispatch(CHEFS_LOADED, res.chefs))

You could mix this loadChefs/dispatch into the action-creator of RECIPE_SELECTED, but that's a really gross mixing of concerns and will quickly weave a tangled web.

You could also do some very imperative (i.e. against the grain for redux) stuff like so (using React):

componentWillReceiveProps(nextProps) {
  if (nextProps.recipe !== this.props.recipe) {
    const { dispatch, recipe } = nextProps
    dispatch(loadChefs(recipe))
  }
}

I really don't like either of these solutions. Thoughts?

Upvotes: 1

Views: 1157

Answers (2)

David L. Walsh
David L. Walsh

Reputation: 24818

Are you familiar with redux-thunk? https://github.com/gaearon/redux-thunk

With redux-thunk applied as middleware, you can do something like this:

function selectRecipe(recipe) {
    return function (dispatch) {
        dispatch(setRecipe(recipe));
        return loadChefs(recipe).then((res) =>
            dispatch(setChefs(res.chefs))
        );
    };
}

Where setRecipe and setChefs are simple action creators. e.g.

function setRecipe(recipe) {
    return {
        type: SET_RECIPE,
        recipe
    };
}

function setChefs(chefs) {
    return {
        type: SET_CHEFS,
        chefs
    };
}

I recommend reading the docs on Async Actions. https://redux.js.org/advanced/async-actions

Upvotes: 4

Lucretiel
Lucretiel

Reputation: 3324

A different solution is to use the Redux-Saga middleware. This lets you write something like this:

function* loadChefsSaga() {
    # takeLatest sets up a system that spawns the inner generator every time
    # an action matching the pattern is dispatched. If the inner generator
    # is still running when additional actions are dispatched, it is cancelled,
    # and a new one is spawned.
    yield takeLatest('RECIPE_SELECTED', function* (recipe) {
        # When a Promise is yielded, the generator is resumed when the Promise
        # resolves. Alternatively, if it rejects, the rejected value is thrown
        # into this generator.
        const {chefs} = yield loadChefs(recipe)

        # Assuming chefsLoaded is an action creator for CHEFS_LOADED
        # `put` is redux-saga's equivelent of `dispatch`. We use it because
        # the saga doesn't have direct access to the `dispatch` function.
        yield put(chefsLoaded(chefs))
    })
}

I'm assuming that you have a basic familiarity with how javascript generators work. If not, go look them up; they're a powerful pattern. In this case, redux-saga uses them to construct functions that can block on things. Every time something is yielded, redux-saga treats it as an "Effect" that it knows how to process. For instance, when a Promise is yielded, redux-saga sets it up so that the generator is resumed when the Promise resolves (or something is thrown into the generator if it rejects).

Upvotes: 0

Related Questions