Action pipeline in Redux Saga

in #programming7 years ago (edited)

work-731198_960_720.jpg


While I was working on one of my React projects, I needed to make sure some of my actions execute before others. Those action were talking to external API endpoints, so naturally I needed to use one of the available middlewares for Redux. I decided to go with Redux-Saga, and it came out to be a good decision.

Actions were data depended between each other. One would produce an API call which will retrieve for example product id that all other actions would later use in their API calls. The problem was, all actions, including the one that fetches product id, are executed in parallel inside saga middleware. They are all dispatched from different components on the same page inside componentDidMount lifecycle method.

All saga watchers are forked, and the code for three example actions looks like this:

First GET_PRODUCT_API_CALL action, retrieves product ID from an external API, and dispatches GET_PRODUCT_API_CALL_SUCCES to update the Redux state with product ID value.

Only after update to Redux state has been finished, other two actions are allowed to execute inside Saga middleware and use the current value of product ID. However looking at the picture with watcher saga definitions, we can see that all actions/api calls will actually run in parallel since getProductWatcher, getFooWatcher and getBarWatcher are forked.

Ideally we would like to make something like this:

diagram.jpg

This shows that GET_FOO_API_CALL and GET_BAR_API_CALL are paused and executed only after GET_PRODUCT_API_CALL has finished, and data they are dependent on is loaded into the Redux state.

So, how do we actually achieve this in Saga?

Saga offers something called ActionChannel. It's basically a buffer for incoming actions, and we can specify which actions will be accepted by this buffer buy defining a pattern which can be a string or a function which accepts action object.

Complete code of action pipeline is in the image below:

So, first we specify a pattern function to match our three actions, using a simple regex to check if the action type has _API_CALL suffix. They will all end up in channel buffer. Then we are constantly looping and taking from buffer one action at a time. Once we step onto GET_PRODUCT_API_CALL action, we will use yield call() effect, which is blocking operation, and we will wait until api call for product is over, and only then proceed.

Very important here is not to forget yield take(Types.GET_PRODUCT_API_CALL_SUCCESS), which is also a blocking call, which ensures that once it's over, product ID will be updated in the Redux state and it is safe to proceed with other actions dependent on product ID value.

When pipeline watcher loop encounters any of GET_FOO_API_CALL and GET_BAR_API_CALL actions, we are calling non blocking yield fork() which will ensure that those two actions always run in parallel.

So, with this, we've achieved our goal. Using different Saga effects such as call and fork and Saga's action channel we've enforced a specific order of execution for our actions. This way we don't have to rely on precise timing, and also unexpected behavior due to poor handling of data dependence between actions.