Managing Application Side-Effects: An Introduction to Redux-Saga
Before you begin…
This article consists of two parts: first understanding side-effects and how they relate to Redux, and then digging into the fundamentals of Redux-Saga. Feel free to skip to the Redux-Saga section if you are purely interested on jump-starting your understanding of Redux-Saga. But if you are still uncertain about whether Redux-Saga is right for you, then the first part of this article may help you with that decision.
What are side-effects?
Redux is growing evermore popular as the primary method of handling state management in UI web applications. Adopting Redux for a project will often pan out like this (especially for developers using Redux for the first time):
- Read and walk through the Redux tutorial
- Write up a few action creators and reducers for a small part of their application
- Try to make an AJAX request and store the resulting data in Redux
- Start searching Stack Overflow to figure out why this simple task can be so confusing
- Question life choices
What they will have just discovered is that Redux has a very limited notion of what is considered “core” functionality. In a pure Redux application, the application follows this sequence:
- An Action is dispatched
- A Reducer changes the Store
This application flow is synchronous and deterministic (because reducers are restricted to being synchronous and deterministic). More often than not, this flow is also completely insufficient for handling all of the possible tasks that modern UI web applications perform.
From Redux’s perspective, anything that occurs outside of that normal flow is considered a side-effect, so it’s entirely up to the developer to decide how they should model and implement those tasks, as well as how they should interact with Redux’s barebones application flow. This includes things like:
- Interaction with asynchronous APIs
- Fetching/posting data via AJAX requests
- Setting timeouts and intervals
- Dispatching actions in response to other actions
For example, let’s say that we have an API client with a
getUser(id) method, which will hit a REST API
/users/:id that returns a user profile. We would like to store that data into Redux so that it can be accessed across our application.
Let’s design the actions and state for this. The first instinct might be to create one action,
GET_USER, and then have a reducer handle that action. However, user data will be retrieved asynchronously (because it is an AJAX request), so we will follows Redux’s advice on designing asynchronous actions and state:
Now that we have a basic set of action creators, we would like to actually wire them up and make the API request (which is a side-effect to Redux). Let’s explore a few options, using this Redux setup as our base.
How can side-effects be implemented when using Redux?
Since Redux is not opinionated about how side-effects are implemented, many patterns have emerged for managing them. I like to divide these into two categories: patterns which operate outside of the Redux lifecycle entirely (Redux-External), and patterns which interleave with the Redux lifecycle (Redux-Centric).
The most straightforward approach to implementing side-effects (and the one that most developers will take at first) is to write and trigger them independently of Redux, and have this independent code call out to Redux (via
Redux-External Pattern: View Framework
Assuming that you are using a framework to render or control the application, it will seem natural to have the side-effect implementation exist as a part of that code. For example, if you are using React, a common approach is to trigger a side-effect when mounting a component, and tie the different asynchronous transitions of the side-effect to Redux via bound action creators:
Analogous patterns can be written using other common JS frameworks, by tying Redux into their respective view or service layers. Without a framework, this pattern would look something like having DOM event handlers trigger the side effect code.
- Can be easier to to understand, by tying side-effect logic to the primary consumer of the side-effect’s results
- Doesn’t involve adding new libraries/dependencies
- Harder to test and reuse code (sometimes; this can be avoided with careful foresight and planning)
- Often adds length and complexity to component/controller logic by adding side-effects to previously pure logic
- Forces side-effects to be tied to the component lifecycle, rather than running independently
- Can’t easily react to events happening in the application flow, such as actions being dispatched
Note: At the time of writing this article, the React Hooks API is still in an alpha stage of development. It looks to be a promising way to make this pattern easier to test and reuse in React, and minimize the added complexity to component code. However, it would still suffer from the downside of having side-effects being tied to the component lifecycle.
Although using a Redux-External pattern is easier to approach, it lacks the ability to trigger more complex side-effects, such as dispatching actions in response to other actions (yes, you could use the
subscribe method, but Dan Abramov says you probably shouldn’t). For side-effects like this, we should use a pattern which ties itself into Redux through some sort of middleware.
The following descriptions of Redux-Centric side-effect patterns are largely inspired by Gosha Arinich’s article 3 common approaches to side-effects in Redux apps, and are re-described here for convenience.
Redux-Centric Pattern #1: Smart Action Creators
In a standard Redux implementation, action creators are pure, meaning they will simply create and return an object, optionally based on some arguments passed to the action creator. In this pattern, we can decide that we want to have action creators also perform the desired side-effects. This is often achieved by having the action creator return something other than a simple object, and using a middleware to handle these non-object actions:
Redux-Thunk is a commonly used implementation of this pattern.
- Action creators are no longer pure, making them harder to understand and test
- Can often lead to callback hell for anything more than simple side-effects (although async-await can help with this)
- Smart action creators can only run when they are called, as opposed to reacting to arbitrary actions
Redux-Centric Pattern #2: Smart Middleware + Specialized Actions
Rather than running side-effects within an action creator, we can move that work into middleware and have our actions provide special instructions for that middleware. This is similar to the Smart Action Creators pattern, except the action creators remain pure, and now a middleware will intercept actions and handle executing side-effects:
Redux-Promise is a commonly used implementation of this pattern.
- Action creators remain pure, so are easier to test
- Harder to generalize to handle any desired side-effect; each type side-effect will often require its own specific middleware to handle
Redux-Centric Pattern #3: Redux Hooks/Listeners
We can take the previous pattern and generalize it a step further, by removing any specialization of actions altogether. If we write custom middleware to listen to generic actions as they are dispatched, and perform side-effects independently of the Redux lifecycle, then our action creators and actions will remain simple and pure, and side-effects will be entirely described by that middleware:
In practice, implementations of this pattern rarely have the developer write each middleware directly; they will often create some abstracted model to develop against. Redux-Saga is a commonly used implementation of this pattern.
- Actions are simple and contain no side-effect business logic
- Allows for many listeners to react to a single action
- Side-effect logic is contained, and if implemented correctly, easily testable
- Implementations of this pattern vary greatly, both in complexity and usefulness
- Can have a steep learning curve
Which pattern should I choose?
As always, this is going to depend on your project’s needs and goals. These patterns are roughly ordered in terms of how they scale (and unfortunately, in order of complexity as well). For small and simple projects, using the Redux-External approach is often sufficient. As your application grows, and more pieces become interdependent, you may want to switch to a pattern that enables better management of your code.
If you’re still reading this article (glad you’re still here!), then you probably are not satisfied with the pattern you are currently using and are interested in seeing what else is out there. Here at Expanse, we have been utilizing Redux-Saga for our applications, and it has helped us organize our codebase and simplify the way that we implement side-effects. It can be difficult to get off the ground, though; the rest of this article is dedicated to making the learning curve more approachable.
Redux-Saga is an implementation of the Redux Hooks/Listeners pattern, and defines a “saga” programming model for handling side-effects.
Sagas are “threads” or “subroutines” which are running alongside the application. Like threads, they can be paused, started, or cancelled from any context (most often using Redux actions). Additionally, because Redux-Saga is built on the Redux Hooks/Listeners pattern, sagas also have full access to Redux state, can “block” on or “wait” for Redux actions, and can dispatch actions of their own.
One of the largest learning curves to using Redux-Saga is that it relies on ES6 generators: a feature of the language that is not commonly used by React/Redux developers.
“Why can’t I write sagas using normal code?”
Generators have a few benefits, such as allowing for writing asynchronous flows in a synchronous manner (similar to async/await). But the main feature is that they allow for sagas to be “continually running” without blocking the main execution thread. Rather than writing blocking calls directly in sagas, blocking calls are described with an effect and “wrapped” with a yield, which returns execution control to Redux-Saga. This allows for Redux-Saga to run sagas incrementally, interleaving execution with the main application (this will make more sense after learning more about generators).
“So how do ES6 Generators work?”
Generators are functions that can pause themselves and return execution control to a “controlling” function (referred to from this point on as the “controller”). They also use 2-way message passing between the generator and the controller to share context between the two functions.The following slideshow walks through a simple example of this controller/generator relationship, to the tune of NumberWang:
“And how does Redux-Saga use ES6 Generators?”
In the above relationship, the Redux-Saga middleware acts as the controller, and the application’s sagas are the generators. Whatever a saga yields will be interpreted as an instruction by the Redux-Saga middleware, and it will return control to the saga as the instructions are completed:
In the previous example, the saga passed a promise to Redux-Saga, which Redux-Saga interpreted as instructions to “resolve this promise, then extract and send back the result”. Unlike async/await, this is not functionality that is built into ES6 Generators. This happens because Redux-Saga’s middleware implementation (controller) decides to follow those instructions for all messages of the promise type.
Redux-Saga treats all messages yielded to it from a saga as an instruction. In fact, it has an entire set of built-in instructions in the library that you can pass to it, and these are called effects. These effects allows for a variety of custom instructions to be sent Redux-Saga, such as waiting for or dispatching a Redux action, waiting for a promise to resolve, or combining multiple effects.
Here’s the previous Redux-Saga example, now using an effect as an instruction, rather than a promise:
“How can I get started working with Redux-Saga?”
Of course, the best way to learn about writing generator functions and using Redux-Saga is by writing some sagas yourself. If you’re hesitant to run
npm i redux-saga right away, I’ve put together an instructional repository with a set of tasks that will introduce you to generator functions, application side-effects, and using Redux-Saga effects and APIs. This repo also contains a “no-sagas” implementation of each these tasks (using the Redux-External pattern) so that you can compare how this approach will change the way your code looks and is organized.
- Redux-Saga Documentation
- 3 common approaches to side-effects in redux apps — Gosha Arinich
- When should I use a saga? — Felix Clack
Looking to flex your newly honed Redux-Saga skills? Expanse is hiring! We help defend the world’s largest organizations by giving them a real-time, comprehensive view of their global Internet attack surface. If you have an appetite to learn and grow, and love solving challenging problems, drop us a line!