Handle Complex Control Flows in React-Redux Apps Using Redux-Saga

Frontend apps are getting big, and it’s becoming difficult to manage their complexity.
React with Redux seems to be the way to go, but it still has one piece not baked in, and it can bite you hard when your app grows.
React with Redux emerged as one of the most notable solutions of recent years. It proves that pure functions, predictable states, unidirectional data flow, and componentization are the right direction to go. But efficient management of side effects (e.g. impure things such as reading from localStorage or asynchronous things such as API requests) is not among the perks of React with Redux. Does this mean you should forget about React with Redux as a solution?
Diverse approaches
The React community came up with many different ways of managing side effects. The most notable are:
- redux-thunk
- redux-observable
- redux-saga
You probably know the first one – it is extremely popular, and its approach of putting logic into actions is perfectly sufficient in many cases. But when control flow becomes complex, a lot of async stuff pops up, and the order of things is important – you can hit redux-thunk’s limits. And it is the reason you should consider using the two latter options.
Redux-observable piggybacks on RxJS, which provides streams – a very powerful way of managing flows. When you get comfortable using it, async flows with many paths, retries and cancellations will be a piece of cake, but the learning curve with redux-observable is quite steep for a regular JS developer and testing the logic of code is not easy, so there are some trade offs.
Redux-saga takes a different approach – their page will describe it best:
It uses an ES6 feature called Generators to make those asynchronous flows easy to read, write and test.
Having a little bit of a steep learning curve (although easier than redux-observable) and despite the quite quirky syntax, I would say it accomplishes the goals pointed out above.
So let's now look deeper into redux-saga.
What does redux-saga code look like?
function* saga(inputValues) {
// await until action is dispatched, get its contents
const { payload } = yield take(ACTION_TYPE);
// call an api function and await it returns resolved promise
const apiResult = yield call(api, payload.whatever);
// dispatch an action
yield put(success(apiResult));
}
Why is redux-saga cool?
Readability
I must admit that after getting used to generators (all the asterisks and yields) and saga-specific wrappers for possible behaviors (function calls, await for actions being dispatched, dispatching an action, etc.), redux-saga code becomes very readable. Because generators can pause the execution of a function, coding the flow looks totally natural. It is very similar to the`async/await` stuff, but we use `function*` and `yield` in the same same manner. Also, we can only `await` promises, but we can `yield` anything that the saga middleware can process – apart from promises we can actually await a particular action being dispatched or the whole subsaga finishes. This results in having code related to one thing in one place.
Complex flow handling
With helpers in redux-saga, you can handle complex flows in a reasonable way. There is a bunch of features that make it easier:
- Cancellation: this is a very powerful feature you wouldn’t be able to get in thunks easily. You can cancel the path of flow, react to it accordingly, and split the code for handling cancellation and errors, which gives you very granular control. For instance, you can quite easily implement rollback logic for complex tasks containing multiple api calls.
- Racing: this feature is quite similar to promises but with one essential difference – the path that loses the race gets cancelled. And do you remember that, analogically to `await`, we can `yield` promises, actions and whatever? Redux-saga makes things such as cancelling api calls due to a user abort as simple as:
yield race({
call(api),
take(ABORT)
});
- Manual control flow: sometimes things get complicated so much that you have to get your hands dirty. Redux-saga provides a bunch of low-level functions for handling the complex branching of logic. For instance, if you want to start a piece of logic that’s quite independent first but merge it later, you can use `fork` and `join` – yielding a fork doesn't await for a subtask to finish. And for high-level things you can complement stuff I mentioned earlier with throttling and delaying baked in.
Testability
Redux-saga’s creators claim that sagas are easily testable. This is due to their nature – they are based on generators and work as middleware collecting metadata of what exactly needs to be done. We can just execute our sagas step by step without even being connected to redux. We can even mock any step of our saga, which makes testing super easy. But there is a caveat – performing tests that way is implementation dependent. A small refactor of a saga can result in broken tests, even if its behavior doesn't change.
Fortunately, tools like redux-saga-test-plan exist and can help you test sagas in different ways.
Interoperability
You can easily use redux-saga along redux-thunk. For example, you can just dispatch thunk from the saga and await it (you don't have to be aware if it's thunk or whatever), so depending on your needs and feels about redux-saga, it is very easy to introduce it incrementally or partially.
It's just a redux middleware and mixes well with different ones, which is one of the beautiful things in redux.
What's Not so Cool in Redux-saga?
Learning curve
The learning curve can be quite steep, especially if you are not used to generators. Saga’s syntax and using helpers that it provides require a bit of a mental shift (but it will pay off later). And we all know that there are plenty of things you have to learn in the react ecosystem – adding another layer doesn't help. If your team already know RxJS, it might be better to go with redux-observable.
It can be overkill
It is perfectly fine to use redux-thunk for handling side effects. You have to choose your tools wisely – redux-saga makes the development of complex async flows easy but requires some time and effort to become familiar. Before you introduce it in a project, make sure that you really need it, and the gains will be bigger than costs.
Conclusion
When your react-redux app grows, starts to handle multiple AJAX requests and perform many operations (especially when time dependence comes into the picture), you need to manage this complexity. I suggest using redux-saga, because of its readability, testability, and complex flow handling, among other things. But remember to use the correct tool for a job – if your app is simple enough, you might live happily without redux-saga. Hopefully, this article has helped you figure out whether this solution is for you.