Promised way to async flows with Redux
Freedom to async when there’s no one true way
Redux is an awesome library for client-side state management. It provides very clear flow of data and events for your application, reducing amount of hard debugging with single store and synchronous updates. Alas, for newcomers this simplicity comes at cost of wondering how to make async flows work.
TL;DR: If you’re just coming to redux and have nice REST-like API to work with, write your own middleware and probably look for redux-sagas in case of complex async flow, i.e. need of frequent optimistic updates.
Redux docs are brilliant, and there’s an entire section about async flow. It shows redux-thunk middleware as an example of handling API calls. Basically, it allows you to get dispatch function in action creator, thus have a way to fire actions with async call results. Don’t know about you, but with new libraries at first I tend to rely on solutions shown on docs pages as the most idiomatic and suitable. In case of thunks from one side and REST APIs from another, this could lead to code more complex than it needs to be. If you’ll go with thunk for each endpoint, you’ll end up with duplicating code for request flow management. Another trouble I’ve come across with was mixing of simple plain object creation in vanilla action creator functions and logic with multiple dispatching in thunks. I’d like to have request handling to be done in one place and to be separated from action creation.
I’d like to walk you through alternative approach. We’ll build our own middleware to handle all API communication in one place, which would be both KISS-assured and idiomatic redux.
If you’re just coming to redux, writing your own middleware seems like hell of a task. Don’t worry, middleware signature is the hardest thing here. For now it’s some function with store => next => action at the top with access to both dispatching action and store.
Let’s assure every action yielding API call has two fields: apiCall and request. First one is function to fire async request, and the second one is object with it’s parameters. Middleware will check each dispatching action for apiCall field and pass further any without one. For actions containing this field middleware will dispatch apiCall is fetching action, start processing api call and fire apiCall success action with request and response fields or apiCall failure action with request and error fields. It also could do some work around authentication and authorization issues, which could be handy to handle in centralized manner. It sounds a bit scary, but the code is quite straightforward.
- We don’t dispatch actions with apiCall property to the store, so their processing ends just here.
- Middleware should go among the first in the middleware chain to prevent fancy things happening to not-dispatched-further actions. Simplest example is logging actions with async calls — they don’t get dispatched to store.
- If you convert promise-based code to async/await note that now you’ll have promise in return from next(action) call and manage this effect accordingly.
- Middleware allows you to put all the things related to handling API calls in one place and reduce (pun not intended) amount of copy-pasting in apiCall functions.
- Request-related action dispatched to store has modified types. I’ve came up with handy utility functions request, response and fail and use them both in middleware and in reducers. They hide constant postfixes and are nice.
Now to more complex scenarios. To fit optimistic changes in this model, you need to update the state with `API request started` action and then either add missing bits from server response on `API request successful` (i.e., set server-assigned id for new entity) or discard optimistic change in case of `API request failed` action. This model seems to be working in case of not frequent syncs, then your sync result comes before next changes on client occur. If you’re not so lucky and you need to sync with API very often, you’ll need to develop some failure management strategy. That’s a situation where redux-sagas comes in handy. In a nutshell, redux-saga provides you generator-based template for managing async flows. It gives you all the control you’ll have with thunks and stays absolutely testable at the same time because it doesn’t fire async calls directly but uses something like task queue to manage them declaratively. Can’t resist mentioning that redux-sagas are not actually sagas and point you to more in-depth article on the matter. Even with this bit of nitpicking, redux-sagas seem like the most advanced approach for handling complex action flows in redux apps.
Don’t be afraid to try your own way from the start — things in redux are much more hackable than they may seem