Redux is a Flux-like framework that has exploded in popularity within the React community. It reduces complexity by enforcing a unidirectional data flow, the use of single state atom, and pure reduce functions for state updates.
For me, there has always been one thorn in the React+Flux setup, which is that more
complicated processes involving coordination of action creators and side-effects are hard to handle.
Solutions using component lifecycle methods (
componentWillUpdate, etc.), and action creators
returning thunks do work, but they seem out of place
in certain situations.
To illustrate what I mean, let’s take a look at a simple Timer app. Note, the source code of the full solution can be found here.
The Timer App
This app will allow users to start and stop a timer, as well as provide the ability to reset it.
We can think of the app as a finite state machine that transitions between two states: Stopped and Running (as shown in the simplified diagram below). While the timer is in Running state, it will update the timer every 1 second.
So let’s go over the basic app setup first, then I’ll show how sagas can help manage side-effects using processes outside of action creators and components.
There are four actions in our module.
START- Transitions the timer to Running state.
TICK- Increments the timer on each tick.
STOP- Transitions the timer to Stopped state.
RESET- Resets the timer back to zero.
State Model and Reducer
The timer state consists of two properties:
status is one of
seconds is the accumulated seconds count
since the timer has started.
Then the reducer is implemented as follows.
The view is straight-forward if we keep it side-effect free. It renders the current time and status, and also invokes corresponding callback functions when the user clicks on Reset, Start, or Stop buttons.
Problem: How to Handle Periodic Updates?
The app can now transition between Running and Stopped states, but there isn’t a mechanism for scheduling periodic updates to the timer.
In a typical Redux+React app, there are two ways we can handle the updates.
- The view can call the action creator periodically.
- The action creator can return a thunk that will dispatch
Solution 1: Letting View Dispatch Updates
For #1, the view will have to wait for state to transition from Stopped to Started, and then start the periodic dispatches. This means we have to use a stateful component.
This does work, but it makes our view stateful and impure. Another problem is that our component is now responsible for more than just rendering HTML and capturing user interaction. It now causes side-effects, which makes the view and application as a whole harder to reason about. This may not be a huge deal in a small app such as this one, but in a larger application you want to keep side-effects at the boundaries of the system.
So what about using thunks?
Solution 2: Using Thunks in Action Creator
An alternative to the view approach is to use thunks in our action creator. We can change the
creator to the following.
The start action creator will now dispatch a
START action as soon as it is invoked. Then, a
is dispatched every 1 second, as long as the timer is still Running.
The issue I have with this approach is that the action creator is doing too much. It is also harder to test this action creator because it is no longer just returning data.
Better Solution: Using Sagas to Manage the Timer
The project redux-saga reifies side effects into artifacts called Effects. The Effects can be generated by another artifact called Sagas. The concept of sagas, as far as I know, comes from the world of CQRS and Event Sourcing. There are some debates on what sagas are, but you can think of them as long-live processes that interacts with the system by:
- Reacting to actions dispatched in the system.
- Dispatches new actions into the system.
- Can “wake itself” using internal mechanisms without actions being dispatched. e.g. waking up on interval
In redux-saga, a saga is a generator function that can run indefinitely inside the system. It can be woken up when a specific action is dispatched. It can dispatch additional actions, and has access to the application state atom.
For example, if we want to dispatch periodic
TICKs whenever the timer is Running, we can do the following.
side-effects and action creators. The
take function wakes the saga up when the
is dispatched. The
put function causes the new
TICK actions to be dispatched. And the
allows us model the wait effect in a structure that does not cause it to run – similar to a Task.
Sagas are a way to manage side-effects within the system. They are a good fit when you need a long-running process that coordinates multiple action creators and side-effects.
Sagas react to actions, as well as internal mechanisms (e.g time-based effects). They are especially useful when you need to manage side-effects outside of the normal Flux workflow. For example, a user interaction could lead to further actions that do not require further interaction from the user.
Lastly, whenever you can model your solution as a finite state machine, sagas are worth a try.
If you want to see the full source code of the Timer app, see the repository here.
Have you tried using sagas yet? What are your thoughts?