Here at Imerso, we have built a web-app with React + redux, to let our users organize their 3D data. We started out with using the redux-thunk middleware in order to perform asynchronous tasks, and fetch to do the HTTP requests. Our typical action creators and reducer looked something like this: (we use immutable.js for our state, but we’ll leave that out for now)
this worked pretty well. Before requesting a resource dispatch an action toggling an
isFetching flag (to show spinners while loading data), request the resource from our back-end, and dispatch an action with the result. But, there’s some problems here:
- What if the request is time-consuming and we want be able to cancel the request? Since fetch returns a promise, and a promise is not cancellable we would have to add some extra state and logic to implement this
- In our app, it is possible for a user to change the current project at any given time. If the project changes after
fetchProjectResourcesis called, but before the promise resolves,
receiveProjectResourceswill put data from one project into the store that is supposed to keep data for another project.
For the most part, the above strategy worked pretty well. In the beginning we rarely hit the corner cases described here, and when we did we implemented some logic to bypass it. However, with time, our app grew and this became rather messy.
redux-observable to the rescue!
Let’s start with a diagram illustrating how redux-observable works on a high level:
In the above diagram we introduce something called Epics which lives alongside our reducer. An epic is a core primitive in redux-observable, and according to the docs:
Epics run alongside the normal Redux dispatch channel, after the reducers have already received them — so you cannot “swallow” an incoming action. Actions always run through your reducers before your Epics even receive them.
An Epic is nothing but a function that takes a stream of actions (typically dispatched from our React components) and returns a stream of actions (which goes to our reducer, which in turn alters the state). These streams are called Observables in the Rx world, and they bring with them a whole lot of Rx-goodies. I won’t cover Rx in depth here (for a good 2 min introduction checkout this post, and I would highly recommend to go through some Rx tutorials).
For now, let’s explain Rx as a framework to deal with events (in this case, actions) over time. It gives us the ability to do familiar stuff like filter, map and even flatMap. However, since we’re dealing with events over time here, we can also do stuff like: delay, debounce, withLatestFrom, takeUntil, etc. This fits really well together with redux, since it gives us the power to handle some pretty complex async stuff without introducing complicated state.
Let’s dive into how we can implement the above example with redux-observable:
Notice that the reducer and our 3 pure action creators remain the same, we’ve only removed the thunk action creator
getProjectResources and implemented it as an Epic.
With the Epic above, we filter out actions from the action stream so we only listen to actions of the type
REQUEST_PROJECT_RESOURCES, then fetch the resources from our server and lastly return an action with the response.
Ok, now what? We’ve implemented our new and shiny Epic, but haven’t yet made any real changes to our program.
Fixing our problems
Let’s try to cancel our request when the user changes the current project:
This is awesome! We simply use the RxJS operator
takeUntil which stops our
ajax.getJSONto emit any values when our epics receives a
SET_CURRENT_PROJECT_ID. That was easy!
Another Rx operator that comes in handy is the
switchMap instead of
REQUEST_PROJECT_RESOURCES actions will cancel any pending requests, and start a new request. We use switchMap quite a lot, especially for network requests that fetches different resources entities, and we are only interested in one at a time.
So to wrap up, if you’re already using redux and are struggling with the complexity of asynchronous actions like we did, redux-observable might be the way to go. I would recommend getting comfortable with Rx(JS) first, which is really great in it’s own right. Happy coding!