The power of Redux-Saga

Einat Bertenthal
Nielsen-TLV-Tech-Blog
8 min readMar 1, 2020

Whether you are new to Redux or not, you probably found yourself struggling with finding simple solutions to complex scenarios. In this article, we will explore Redux-Saga and the elegant solutions it introduces to solve those complex scenarios.

Redux is synchronous

As you probably know, Redux manages your application state synchronously.
One of the main concepts of Redux is reducers.

The reducer is a pure function that takes the previous state and an action, and returns the next state. It’s very important that the reducer stays pure. Things you should never do inside a reducer:

Mutate its arguments;
Perform side effects like API calls and routing transitions;
Call non-pure functions, e.g. Date.now() or Math.random().

Side effects?

A side effect is any code that runs asynchronously or talks to an external source to the application, for example:

  • Talking to a backend server
  • Logging (Splunk)
  • Collecting analytics data (Mixpanel, Google Analytics)
  • Accessing the browser’s local storage

Middleware to the rescue!

So what can we do if Redux can’t run side effects? We will use a middleware!

middleware provides a third-party extension point between dispatching an action, and the moment it reaches the reducer

Middlewares don’t come out of the box with redux. It is usually a package that we will install, or we can write one for our selves.
But how do we choose a good middleware? Based on what criteria?

Attributes of a good redux side-effects middleware

  1. Access to the Redux store. This is helpful when we want to make some decisions based on our application’s state.
  2. Ability to dispatch another action from within the middleware. This will allow us the flexibility to trigger other side effects that have different business logic.
  3. Ability to run side effects — obviously.
  4. Cancel side effect. Since side effects can be asynchronous, we might want to be able to cancel the asynchronous process before it ends and affects our application’s state. (for example, the user decides to cancel file uploading before it finishes uploading)
  5. Allow the user to trigger some action multiple times (like clicking a refresh button). In this case, we want to be able to cancel all previous side effects and always keep the latest side effect running, to avoid unnecessary processing and to consistently correlate with the user’s actions.
  6. Run the same specific side effect for different dispatched actions.
  7. Debounce: delay the invocation of a side effect until after some milliseconds have elapsed since the last invocation. For example: autocomplete side effect.
  8. Throttle: regulate the rate at which your application’s side effects are running, meaning prevent our side effect from running more than once every X milliseconds. For example, you can regulate the rate of a refresh button click.
  9. Race: sometimes, we would like to race between multiple side effects, and when one of them finishes, we want to cancel all the rest since they are now redundant.
  10. All: run multiple side effects in parallel, wait for all of them to finish, and only then do some other action.

Redux Side-effect Middleware Statistics

Some of the most popular redux side-effect middlewares are redux-saga, redux-thunk, redux-observables, and redux-promise.
If you checkout npm trends and compare between all of the popular redux side-effects middlewares, you can see that redux-saga has the highest number of stars and forks, and it is highly maintained.

NPM Trends

Redux-Saga

“Sagas are implemented as generator functions that yield objects to the redux-saga middleware.”

Generators

Generators are a core concept in Redux-Saga, so before we talk about Redux-Saga, we must understand what generators are, how they work, and how to use them. If you are already familiar with Generators, you can skip to Redux-Saga setup

So what are generators?

  • A generator function is defined by the function* declaration (function keyword followed by an asterisk).
  • Generator functions can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances.
  • Generator function returns a Generator object, which is a kind of Iterator.

Let’s take a look at some basic examples:

If we look at the above example, we can see that calling the generator function does not execute its body immediately. Instead, an iterator object for the function is returned. When the iterator’s next() method is called, the generator function's body is executed until the first yield expression, which specifies the value to be returned from the iterator.

The next() method returns an object with a value property containing the yielded value and a done property, which indicates whether the generator has yielded its last value, as a boolean.

On our example, since i is equal to 10:

  1. Calling next() on the first time executes the first yield , which returns {value: 10, done: false}. done is false since the generator is not done yet.
  2. Calling next() the second time, we return to the generator function in the exact place we left it and execute the function until we reach the second yield . The second yield returns {value: 20, done: true} since i + 10 is equal to 20, and the generator is not done yet.
  3. Calling next() the third time will return {value: undefined, done: true} since the generator has already yielded its last value.

Note that we can call next() whenever we want, which means that our generator function has async flow, but you can clearly see it is also synchronous-looking. Looks a little like async/await, right?

In the above example, we will see how we can pass data in the middle of the generator iterations:
On the third time, we call next() the returned object is {value: 1003, done: false} because i equals to 1000 and then we pass the value 3 to the next method, hence 1000 + 3 yielded 1003.

Wait. What?? 🤔

Here is why: calling the next() method with an argument will resume the generator function execution, replacing the yield expression where execution paused with the argument from next().

It’s also important to note that Generator functions can call other generator functions from within them.

Ok, so now that we’ve learned how to use Generator, we are ready to learn about Redux-Saga! 💪

Redux-Saga setup

When we create the Redux store, we also create our Redux-Saga middleware and connect it to the store via applyMiddleware. After the store was created, we call run with our root saga, which starts our redux-saga middleware.

Watchers and Workers

  • The main saga file is usually split into two different types of sagas: Watchers and workers
  • Watcher saga sees every action that is dispatched to the redux store; if it matches the action it is told to handle, it will assign it to its worker saga
  • The worker saga is running all the side effects it was meant to do
  • The watcher saga is typically the root saga to export and mount on the store

Simple saga example

In the above example, the watcher saga is listening to GET_ITEMS_REQUEST_ACTION. When this action type is dispatched, the watcher calls getItems saga. Then, Redux-Saga will start executing getItems function. On the first yield, we call some API, and on the second yield, we dispatch another action of type GET_ITEMS_SUCCESS_ACTION and payload containing the result of the previous yield.

call and put are effect creators. We will talk about effect creators shortly.

Remember the “Attributes of a good redux side-effects middleware” list we mentioned before? Let’s see how Redux-Saga implements these attributes!💪

Effect Creators

Redux actions which serve as instructions for Saga middleware

  • select: returns the full state of the application
  • put: dispatch an action into the store (non-blocking)
  • call: run a method, Promise or other Saga (blocking)
  • take: wait for a redux action/actions to be dispatched into the store (blocking)
  • cancel: cancels the saga execution.
  • fork: performs a non-blocking call to a generator or a function that returns a promise. It is useful to call fork on each of the sagas you want to run when you start your application since it will run all the sagas concurrently. (non-blocking)
  • debounce: the purpose of debounce is to prevent calling saga until the actions are settled off. Meaning, until the actions we listen on will not be dispatched for a given period. For example, dispatching autocomplete action will be processed only after 100 ms passed from when the user stopped typing.
  • throttle: the purpose of throttle is to ignore incoming actions for a given period while processing a task. For example, dispatching autocomplete action will be processed every 100 ms, while the processed action will be the last dispatched action in that period. It will help us to ensure that the user won’t flood our server with requests.
  • delay: block execution for a predefined number of milliseconds.

Effect combinators:

  • race: a race between multiple sagas. When one of the sagas finishes, all the other sagas are canceled. similar to Promise.race([...])
    fork and race are used for managing concurrency between Sagas.
  • all: run multiple Effects in parallel and wait for all of them to complete. similar to Promise.all

Helpers:

  • takeEvery: takes every matching action and run the given saga (non-blocking)
  • takeLatest: takes every matching action and run the given saga, but cancels every previous saga that is still running (blocking)

Let’s compare two saga examples to understand it better:

In this above example, we yield call. call is a blocking effect creator. This means that the saga will not continue to run to the next yield until the API call finishes. Once it’s finished, we yield put. put is dispatching a new action with the result from the previous yield. put is non- blocking.

In this example, we call put with some action. put is a non-blocking effect creator, so it dispatches an action (could be an action that triggers some other saga), but the saga is not waiting for this action to finish. The saga is free to run to the next yield. the next yield is calling take. take is a blocking effect creator. It is waiting ASYNC_ACTION_SUCCESS to be dispatched. Only when it is dispatched, the saga continues to the final yield, that calls put, which dispatches an action with the payload returned from the previous yield.

If we compare these two examples, they are doing the same thing. the only difference is that on the first example we are blocked until the async action finishes, while in the second example, we can do whatever we want until we decide to wait (take) for a success action to be dispatched.

Now that you can write simple watchers and workers in redux-saga, it’s time for you to take a look at how you can implement a file upload mechanism using the race effect. 🤓

Tests

The Effect Creators return JS Objects — in unit testing, we can compare that object’s value to the expected value

There are some useful libraries for testing redux-saga:

  1. redux-saga-test
  2. redux-saga-testing
  3. redux-saga-test-engine
  4. redux-saga-tester
  5. redux-saga-test-plan

If you are looking for a deep dive into the above libraries that cover the different approaches to testing sagas, read this great article by Sam Hogarth.

That’s it! I hope you enjoyed reading and are ready to use Redux-Saga in your codebase! If you have any questions, feel free to ask.
I’m here and also on twitter.
Thanks for reading! 🙂

--

--