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:
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.