Exploring Redux Sagas
Redux-sagas are the hot new (or, if you’re reading this a few months from now, the old school) way of working in the redux world, solving any ailment that you may care to have in your application. Well, at least if your problem is centered around: ‘I have some non-trivial update I want to perform, or a side effect that I don’t want to tie to a component, how can I do it?’. At Onfido we use React/Redux and also encounter that issue, so it made a lot of sense to explore sagas to see how well they solved it in practice!
A detour into ‘why is this even necessary?’
One issue that a React/Redux developer will encounter, sooner or later, is performing a change when a component does not naturally have all of the information available. Whilst it’s certainly possible to just pollute the component with the extra data, it would be cleaner if we could avoid that entirely; allowing the component to remain focused on displaying data.
A popular solution to this problem is the redux-thunk library. Redux-thunk is a middleware which allows you to dispatch a ‘thunk’. The thunk is a function which returns another function with dispatch and getState passed in as arguments.
Once you have access to these two functions, you are free to do all kinds of fun stuff like select data from the store, dispatch actions, and perform AJAX requests, all without the component having to be aware of the implementation. Therefore the component could get back to being focused on its primary job of displaying information to the user, and responding to interactions.
Taking a simple, albeit contrived, example: let’s say we have a component,
Robot whose lot in life is to pass butter. It does this via displaying whether or not the
butter.hasMoved props is true, and also dispatching an action,
MOVE_BUTTER, which will set the butter.hasMoved value to true on the store. For illustration purposes:
Butter reducer shape:
It makes sense that the robot is aware of the data contained in the butter reducer. It needs to know whether or not the butter has already been moved in order to determine whether or not to allow the butter to be moved. The Robot isn’t aware of exactly where this information is stored, as this particular detail is handled by the selectors.
So, time to take it further! It’s entirely possible that another part of the application, let’s say a reducer called
genius and a related component called
Scientist, have an interest in when the butter is moved. This reducer maintains an object, of which there is a property
When the robot has moved the butter, we would like to update this object so that the
canPraiseRobot prop is set to false. If the Robot component were to perform this update, it will then know more about its surrounding environment that it needs to. After all, its job is to pass butter, not to try and make friends. However, that other part of the store still needs to be updated somehow.
Without any form of middle layer between the React layers and the reducers that will eventually handle the action, we’re pretty limited on how to handle this functionality. A common solution (that doesn’t simply leak implementation details of what happens after moving butter all over the poor robot) is for a parent component to contain all of the dispatches and selectors for both Scientist and Robot and pass them to both components. The moveButter function (now defined in the parent) will then be able to perform all of the necessary updates, without the robot needing to know about the actions required by the genius reducer:
Whilst this can work in some situations, this isn’t always ideal. The stateless components are still composable and reusable, but they are now linked by a parent providing the functionality we need. The implementation of functionality is dictating the position of components in the UI.
Depending on the situation (are the components naturally close together in the UI structure? are they several layers apart?), this may not even be feasible. Real applications can often reach this boundary, where either compromises are made (sometimes unconsciously), or different approaches are required.
What about context? One other method of sharing data around in React is, of course, context (A popular example of context is react-redux’s Provider). It’s potentially very useful, but it’s recommended you avoid using it unless you have specific reasons to do so (which is usually only going to be when creating a library, rather than ‘application code’). Also, whilst critical to the functionality of pretty much any react/redux application, it’s an unsupported feature which could, in theory, disappear one day.
A nicer solution is to take the burden of stitching together this kind of functionality from React. React already has to govern the UI structure, presenting data in a sane way. We need some form of glue code to link the React and Redux layers in a way that gives us freedom to perform the updates we need to build our fancy modern web application.
This glue can take many forms (RxJS observables, for example), and this is the layer where thunk/sagas reside. Both of these provide middlewares which hook into the dispatch functionality of redux, but go about handling dispatching and selecting of data in a different way. Both of these have access to getState and dispatch, with sagas opting to hide them behind ‘effects’, rather than directly exposing them. Both are able to take an action and perform any additional side effects needed in an application, including dispatching further actions. Instead of relying on passing unnecessary props around, or creating parent components which don’t give you the freedom you need, you can instead create one function/generator which describes the functionality in it’s entirety.
Below is an example of a saga doing just that:
This neatly solves the major issues with the previous approaches. Since this glue code has no interest or bearing on the structure of the UI directly, there is no longer any need to group the components. Each component is now able to have its own selectors and actions, providing just the data the component needs, and no more than that. What the actions do, once triggered, are of no interest to the component. It’s not the component’s job to know. Like the butter passing robot, the components have a very specific role — they display data and handle UI interactions (which will nearly always result in an action being dispatched).
This allows for easier creation of composable ‘connected’ (i.e. connected to redux) components. They’re focused on one task, they only utilise props directly relevant to them, and as long as you make sure your action types are easily traced (e.g. via static imports of the type constant for sagas, or importing and dispatching the thunk itself for redux-thunk), it will always be easy enough to find the functionality the action will trigger.
A common example of a saga which would potentially perform many updates would be any form of http request. For example a simple request saga could:
- Dispatch an action to update a reducer related to the loading status of the app
- Perform the request
- If it failed, dispatch an action to a reducer related to global notifications in the app, or call another saga which handles request errors.
- If it succeeded, dispatch an action to update the store
- Dispatch an action to update the loading status of the application again.
Of course, all the component did was dispatch the relevant action, it didn’t need to know any of the implementation details.
In tl;dr form: You’ll probably want to have a ‘glue code’ layer between your React and Redux layers if you’re aiming to perform complex functionality as a result of action dispatches. Sagas are one way of creating this layer.
Why should I use sagas as my glue code?
As a bit of background: after a brief stint of thunk-less React (of a couple of months), I adopted redux-thunks for the reasons outlined in the last section. It made life so much easier, allowing me to seperate my code into nice (fairly) distinct layers; React, ‘glue’, and Redux. It all worked pretty well; nothing cropped up to make me actively dislike thunks. In fact, when I was first reading through the (pretty good!) redux-saga documentation, it was from a viewpoint of cynicism; thunks were working, and sagas seem to be involved typing out more lines of code! They also needed to be manually ‘wired together’ through a series of yields. Thunks, by comparison, are just imported and called directly by the components using them, so are much less effort.
So why bother? If it’s not going to make my life easier, or at least provide me with some bonus functionality as a trade off, then it’s not worth it! For me, the major tipping point that pushed me into trying them out was the ease of testing sagas (more on that later). But I stayed for the easy composition, and the interesting ways in which sagas can interact with each other.
The fun of composition
One of the most powerful aspects of sagas is the ability to very easily combine various competing sagas/effects. This made situations such as cancelling multiple xhr requests much more simple than the thunk equivalent would have been. Let’s have a look at a fairly common pattern; we have 3 requests for fetching all of the data we need for a given part of our application. If any of them fail, we have no interest in carrying on with the others (if you need to go out to buy more butter for the Robot, there’s no point in continuing a search for a store if you failed to find your wallet). We also want the ability to cancel them (say, if the robot exploded).
Here’s an example of what that saga may look like:
Using a saga effect called
race, we pit three effects against each other in a fight to the death. Three effects enter, only one leaves. One of them is our array of requests for loading all of our data. One is a take effect listening for an action indicating some form of error has occurred, and the other is a take effect listening for a cancel action.
When the winner of a race effect (i.e. the first one to complete) is known, the others effects are remorselessly cancelled (Note: this will not automatically cancel any xhr request). If the effect involves calling multiple other effects (like in our request property), then all of those effects will be cancelled too. In the example above that means that all three of our ‘fetch’ sagas will be cancelled if the cancel or error actions are dispatched.
Upon completion of the race, the race effect returns an object, where only the winning property will be populated; the others will be undefined. By destructuring the object, we can then choose to act further if certain effects were the winner. In requestButter() we’re not paying attention to the request property (as the fetch sagas, in this simple example, are handling the store updates themselves), but we are interested if the saga was cancelled or failed, so they’re destructured and referenced later on.
The result of this pattern is that the only thing we need to do to cancel an ongoing group of sagas is to simply dispatch the correct cancellation action! This is just an action like any other, so you are completely free to dispatch an action from a componentWillUnmount() method, or from any other part of your application, without having to pass references to the cancellation tokens of these specific requests around.
We’re not quite done yet though! Cancelling a saga doesn’t mean the xhr request will be cancelled too. All that the saga cancellation will do is prevent the application from doing anything useful with the response. It’d be like ordering a pizza, but being extremely impolite and getting eaten by a rogue lovecraftian horror before it gets delivered. The pizza preparation and delivery is now wasted effort (although you’ve got bigger issues, being digested and all that). Wouldn’t it be far better if that pizza delivery would be cancelled upon your consumption, to prevent all that wasted effort and the mental scarring of the person delivering it?
Fortunately, sagas make this pretty easy too:
Using the finally block, we can handle any ‘cleanup’ functionality that will need to run when the saga completes. To figure out if the saga was cancelled (therefore avoiding unnecessary cancel token calls) redux saga provides a ‘cancelled’ effect. Using this handy effect, we can reliably cancel our xhr request whenever the saga is cancelled.
What’s neat about this pattern is that there’s no need to leak the cancel token anywhere else in your application; it is all handled within this one saga, avoiding the need to leak the xhr cancellation implementation. There’s also the benefit that, should you decide you’d quite like to reuse this saga elsewhere, there’s no extra setup required to enable cancellation of the xhr request. If the saga is cancelled, so is any ongoing request.
At the moment, the simplicity of request cancellation is the largest single benefit I’ve yielded from sagas. Considering it’s a fairly common pattern, that’s no small thing!
Sagas are yielded from other sagas (which is why you need to separately ‘wire them up’ in your application), with the ‘root saga’ hooking into the store’s dispatch mechanism via middleware. Sagas then listen for specific action types using the take effect.
The reliance on actions allows you to easily trace when certain sagas are being triggered using the redux dev tools. Combined with the actions you’d usually have in your application, and some sensible prefixes, Redux dev tools transforms into a fairly detailed reference for your application’s behavior history. Bugs are much easier to track down if you know at a glance which saga was the last one to run before the bug occurred!
This has an added bonus of neatly separating the sagas from the React layer. The connected React components do not need to have any awareness that a saga is going to act upon the action, they’re just dispatching a message. By comparison, for thunks, the thunk itself is called by the connected component, and the thunk itself eventually dispatches some action. If, for some reason, the redux-saga layer simply disappeared from the application, the React components would still work perfectly fine — there’d just be nothing listening to the dispatched ‘saga actions’.
Depending on how far you want to take sagas you can either route every action through the saga layer, including fairly standard store updates, or dispatch actions to sagas only when you would like to perform more complex functionality. There is a lot of freedom in this regard and, as long as the action types are sensibly named, it should always be fairly easy to see what is happening in the application (along with what is likely to be listening to a given action).
I’ve personally been leaning towards the ‘sagas do everything’ approach. It has the added benefit, given sensible file naming, of providing an at-a-glance overview of an application’s functionality by simply opening the sagas directory. It also allows me to easily enhance functionality if desired, as the saga already exists.
Of course, your mileage may vary, so it is up to you to find a balance you’re happy with!
I mentioned previously that one of the compelling features of redux-sagas was the promise of easy testing. The library itself performs admirably in this regard; all of the redux-saga effects, when yielded, are just objects which can be easily tested. Unfortunately, testing sagas is still a bit of a chore, to say the least.
The library attempts to mitigate a lot of issues with testing such side effect heavy functionality (not having to mock function calls is great!), but it doesn’t change the unfortunate truth that writing tests for a generator, used in this fashion, is often painful. With generators, the order of yields can, and often do, matter. The ideal functional tests are ones that test input versus output, but don’t strictly care about implementation; whenever the function changes, the tests provide a useful way of verifying that the output is still correct. However, testing a saga is nearly always about implementation (did the ‘isLoading’ property get set to true at the start, and then set to false before the saga ended?). And, true to form with any implementation based testing, such tests are very likely to break when making any change to the saga. Even if the saga is still functioning correctly!
Test maintenance of sagas becomes arduous, and I’m currently on the fence as to whether it’s worth the time investment to attain high levels of coverage. Sagas tend to be the ‘brains’ of the application that tie everything together, so having those tests is certainly justifiable; if we compare it to event-less stateless components, there is much more at stake (from a functionality point of view) if a saga is doing something wrong. It’s also more likely a severe saga issue will slip by too, especially if it’s a relatively niche bit of functionality that doesn’t get called all that often. On the flip side, saga test writing can often boil down to simply being a check of how the saga was built, without it necessarily revealing whether or not it is correct. As a result, tests are simply not as reliable an indicator of validity than they are for, say, a mapping function.
I don’t think this is a fatal blow for saga testing; it’s just still early days and ‘best practices’ aren’t readily available. Over time, this situation will hopefully improve and we’ll see more reliable testing patterns for sagas. Also, as previously noted, sagas do usually contain extremely important functionality, so it may be a case of ‘eating your vegetables’ and writing the tests anyway, even if they are brittle.
My initial impressions of sagas were pretty positive. Whilst I’ve not yet pushed into functionality far beyond anything I simply couldn’t achieve sensibly with thunks, I’ve felt that they’ve cleaned up the codebase and made some existing patterns (such as request cancellation) much easier to work with. It was at the expense of some extra lines of code, but I’ve so far felt that the benefits I’ve been encountering have been worth it. Still, I’ve only begun to scratch the surface of using sagas. I see potential for them to prove extremely useful in complex workflows, due to the ability to easily yield and combine other sagas. Of course, the challenge will be to see if that potential really does translate into building robust complex applications, but I’m looking forward to experimenting!
Finally, of course, the question: ‘should I be using sagas?’ Well, I can definitely recommend giving them a go. They’re a step up from thunks, and I think they do a great job of separating out the codebase into manageable layers. But, of course, it’s always worth bearing in mind that the JS world is a hugely volatile one right now. Whilst sagas are pretty popular, there’s no guarantee they will last long term. This the case for all libraries (even the big ones!), so I tend to judge a library by how pretty a fossil it will be when developers in 3–4 years look at the codebase and they’ve no idea what a ‘saga’ even is. In that respect I think the separation of the layers, and resulting simplicity, make sagas a fairly safe choice for adoption right now for building complex applications.