Handling Side Effects in Redux: Redux-Saga

Handling Side Effects in Redux Redux-Saga

Handling Side Effects in Redux Redux-Saga

In this article you will learn the following:

1. How side-effects are handled in Redux
2. What Redux-saga is
3. What generator functions are
4. How to handle side-effects using redux-saga while building a simple application

If you have used Redux in a React project before, there gets a point, where you will have to handle side-effects(asynchronous) like API calls or setting timeout and intervals, because these are required when building features of a “complex web application.”

The Redux architecture is meant to provide predictable state change, meaning it takes an action and state then returns a new state. This architecture doesn’t provide any built-in means for handling asynchronous changes to the Redux state tree.

Redux flow

Redux flow

So where can we put our side-effects in the Redux flow? We could handle our side-effects at the component level then dispatch an action with the resolved result of the side effects.

But the problem with this approach is that in large applications, you might have multiple components that want to fetch a user, so you might have duplicate logic, and this becomes difficult to maintain.

Another option is to move the async logic(side-effect) to the action creators. Usually, in redux, action creators are functions that return an action; a plain object that must have a type property, at least.

We can use a middleware like redux-thunk which allows you to dispatch functions/promises and write async logic in your action creators. What the middleware does is look at every action dispatched, if it is a function, it calls that function and pass two arguments; the store’s dispatch method, which can be used to dispatch plain action objects when the asynchronous code has failed or succeeded and the store’s getState method is passed to it so the store can be read and used in the action creators.

Here, we have the fetch user API call in the action creator, so any component that needs to fetch a user can dispatch the requestUser() action instead of duplicating logic.

Using a middleware like redux-thunk has its benefits:

  • The business logic is not tightly coupled to the UI components so it can be reused by components.
  • It is easy to learn unlike redux-saga(which we will be covering later in this article) because you don’t need to learn new JavaScript concepts to use it.

It also has some disadvantages:

  • It can become difficult to read and understand when you start dealing with complex async login in complex applications.
  • It doesn’t provide any way to handle advanced async operations like cancellation and debouncing.

Despite its negatives, Redux-thunk remains the most used redux middleware for handling side-effects, and it works for most use-cases, but it doesn’t work in complex applications where you might want to cancel an API request after an action has been dispatched, or you want to debounce a function call.

This complex situation is where a middleware like redux-saga thrive, it helps you handle these easily using special function called effects provided by redux-saga.

Redux-saga also allows us to write async code in a declarative manner hence it is more readable and easier to test. Now we will do a deep dive into Es6 generators and Redux-saga, then we will build a trivial application using redux-saga middleware.

Redux-saga

Redux-saga is built on the concept of generators which was introduced as part of the ECMAScript 2015 module. Generators are a powerful concept, which you might not use every day in programming but its strengths were leveraged to create Redux-saga, which then gives users the ability to write asynchronous code in a synchronous manner, thereby eliminating problems with callbacks(callback hell), as well as helps to run side-effects in parallel, cancel them, etc. Both of which are difficult or impossible using redux-thunk.

What is a Generator function?

A generator function is a function that can be paused and later resumed while retaining its variable bindings(context). It is denoted with an asterisk in front of the function keyword.

Unlike a normal function in JavaScript, which runs until it gets to a return statement or until it has completely executed, generators run until it gets to a yield or return statement.

When a generator function is invoked, it doesn’t execute the function body immediately, instead — it returns an iterable/generator object that can be iterated or looped through using the iterator’s next() method or a for..of loop.

Each iteration is defined by the yield keyword, so when you execute a generator function and call its next() method, it runs the function until it encounters the first yield statement, then it pauses and returns an object containing a value and done property;value is anything on the right-hand side of the keyword yield, it can be a value(integer, string), function, object, promise, etc., while the done property is a boolean, which indicates if it is done iterating thorough the iterable.

So how does this fit with redux-saga? In redux-saga, we move our side-effects from the action-creators to generator functions called sagas. What redux-saga does is to run our sagas(generator functions) and provide methods for handling the side-effects and also interacting with the redux store. redux-saga executes our generator functions(sagas) containing the side-effects then handles the process of calling the next() on the generator object internally.

Below is a simple saga that makes a cup of coffee after 10 mins:

This is just a simple snippet of a saga that yields objects to the redux-saga middleware. The yielded objects are like instructions to be interpreted by the middleware. You might have noticed functions take, delay and put in the saga above, these are helper functions provided by redux-saga. We will walk through each line of the saga to understand what it does, then I will show the redux-thunk implementation.

The first line in the example above:

take is an effect that instructs the redux-saga middleware to wait for the REQUEST_COFFEE action to be dispatched by the store before it resumes execution of the generator function. So this function is paused until someone dispatches a REQUEST_COFFEE action; then when it is dispatched, it resumes execution and executes it until the next yield.

delay is a function that returns a promise so when a promise is yielded to the redux-saga middleware, the middleware pauses the saga until the promise is resolved then execution resumes after 60 seconds. As I said earlier anything can be yielded by a generator function, it can be a function, promise, or value. But what redux-saga does here is that when it is yielded a promise, it has to wait for it to be resolved. This is quite similar to the async await way of handling promises.
So after 60 seconds, execution resumes until it gets to the final yield

put is another effect provided by redux-saga which can be used to dispatch actions in a saga. So this instructs the middleware to dispatch an action COFFEE_DONE to the store.

put and take are examples of helpers effects provided by redux-saga. Effects are plain JavaScript objects which contain specific instructions to be carried out by the middleware.

Effects are divided into two groups in redux-saga, blocking call and non-blocking call.

blocking call means that the saga yielded an effect and will wait for the outcome of its execution before resuming execution inside the generator function.

non-blocking call means the saga will resume immediately after yielding the effect.

Blocking vs Non-blocking calls

Blocking vs Non-blocking calls

There are more helper effects, but these are some of them. You can check out the redux-saga docs for all of them.

Demo Application

Now that we have a little knowledge of generators and redux-saga, we will be building a simple application that calls the Jikan API, an unofficial API for MyAnimeList, to get the top animes airing and also search for your favorite anime.

This is what the application will look like

Live Demo of the application

Folder Structure

index.js

This is the entry point of our application. In this file, we wrapper the App component with the react-redux Provider, which gives the App component and any component below it in the component tree access to the redux store if it is wrapped using connect() function.

store.js

In this file, we set up the redux-store, the redux-saga middleware, and the redux-devtools.

This line creates the store using redux createStore function, the first argument passed to it is the root reducer while the second argument is the enhancer, and it composes the redux-devtools and the saga middleware together.

This line above dynamically runs the sagas, and it takes a saga as an argument.

App.js

This is the parent component of our application, when it is about to mount it dispatches two actions to the redux store; requestTopAnime which requests the top airing animes and requestSearchAnime() which requests the top animes of all time. requetSearchAnime() is also used to search for animes if a query argument is passed to it.

This component has a search field so you can search for your favorite anime and two other components AnimeList and TopAnimes.

AnimeList.js

This component displays the search results if there is a query, or the best animes if the query field is empty. We connect this component to the Redux store using react-redux connect() function. Also, we have a state selector getSearchResults that is used to select the part of the state needed in this component.

TopAnimes.js

It is similar to the AnimeList component, but it renders the lists of top animes airing. It has a state selector getTopAnimesResults for selecting the relevant state it needs.

actions.js

This file contains our action creators that returns plain JavaScript objects.

Reducer.js

The reducer.js file contains two reducer:

  • searchResults: This is where the state of the search results is managed
  • loadingSearchResults: This is used to manage the loading state of the top airing animes.

Api.js

The file contains two functions that return promises.

  • fetchTopAnimes:  This function makes an API call to get the top airing animes and then returns a promise.
  • fetchAllAnimes:  This function makes an API call to get the results of the search query if there is a query, or it gets the best animes of all time if there is no query string. It also returns a promise.

saga.js

This is the file where all the async calls/side-effects are being handled. In redux-thunk the async logic is put in the action creators, but in redux-saga, we have a dedicated file for this usually called sagas.

The saga file is usually structured in a way that we have two types of generator functions(sagas):

  • Worker Function
  • Watcher Function

The Watcher function waits/watches for a specific action to be dispatched to the redux store, then it calls the respective worker function which handles the side-effects/API calls

The two watcher functions are:

  • getTopAnimeWatcher
  • getSearchAnimesWatcher

The getTopAnimeWatcher yields an effect called takeEvery(), which takes an action and a function as arguments. takeEvery is a redux-saga helper effect that tells redux-saga to continuously and concurrently wait for an action of type TOPANIME_REQUEST to be dispatched, immediately it is dispatched, it should execute the getTopAnimesWorker function which handles the side-effects. We have another similar helper effects to takeEvery that can be used to listen for actions dispatched called takeLatest.

takeLatest calls its worker saga every time an action’s dispatched and it automatically cancels any previous execution of the worker saga if it is still running, while takeEvery does not cancel any previous saga.

The getSearchAnimesWatcher is similar to the getTopAnimeWatcher. When an action of type SEARCH_REQUEST is dispatched, it calls the getSearchAnimesWorker function.

We now know that the watcher functions call the worker functions, so what happens in them?

In the getTopAnimesWorker function, it first yields a function call that takes the fetchTopAnimes function we exported from the Api.js file.

call is another helper effects provided by redux-saga, it is used to execute/call a function but if that function is a promise it pauses the saga until the promises are resolved. The call effect is like await in async-await syntax.

So what the line above does is call the fetchTopAnimes async function, wait for it to be resolved, then the response is saved in the data variable. if the promise failed then it is caught in the catch block of the try-catch.

if the promise was successful execution of the saga resumes to the next line:

put is another helper effect which is used to dispatch an action to the redux store. The line above dispatches a fulfilledTopAnime() action with the response as an argument, so the reducer can update the store state.

If the promise failed then execution continues in the catch block

This dispatches an action to the store to indicate the request failed.

getSearchAnimesWorker is similar to the getTopAnimesWorker function, the only difference is that the call effect takes a second argument.

This second argument makes it possible to pass an argument to the fetchAllAnimes function, so it looks something like this when it is called

Apart from that, everything else is the same as the getTopAnimesWorker we talked about earlier.

Finally, there is one more function in the saga file we have not discussed yet; the rootSaga generator function. This function uses another helper effect all. This effect tells redux-saga to run the functions in parallel.

The rootSaga function runs the two watcher sagas in parallel, so it can be exported and ran by the saga middleware. Basically, it connects the sagas to the redux-saga middleware

Above is a line of code in the store.js file that runs the saga.

This is just a trivial example to show you how to use redux-saga, but it is obvious there are benefits of using it instead of redux-thunk.

Some benefits of using Redux-saga:

  • Easier to test because we don’t have to mock API calls
  • Looks cleaner because we don’t have to deal with callbacks and asynchronous tasks are achieved in a synchronous nature.

Conclusion

Redux-saga might be an overkill for the trivial example, but it thrives when you need to handle complex asynchronous tasks in Redux. Personally, I use redux-saga for most of my Redux-based projects, because it looks a lot cleaner and easier to understand than the promise-based redux-thunk.

Github: https://github.com/nero2009/Anime-viewer

Here is an illustration of the flow:

Flow with sagas

Flow with sagas

References

About the author

Stay Informed

It's important to keep up
with industry - subscribe!

Stay Informed

Looks good!
Please enter the correct name.
Please enter the correct email.
Looks good!

Related articles

15.03.2024

JAMstack Architecture with Next.js

The Jamstack architecture, a term coined by Mathias Biilmann, the co-founder of Netlify, encompasses a set of structural practices that rely on ...

Rendering Patterns: Static and Dynamic Rendering in Nextjs

Next.js is popular for its seamless support of static site generation (SSG) and server-side rendering (SSR), which offers developers the flexibility ...

Handling Mutations and Data Fetching Using React Query

Utilizing React Query for data fetching is simple and effective. By removing the complexity of caching, background updates, and error handling, React ...

1 comment

Nikita Bragin December 6, 2019 at 11:26 am
0

Try Redux Observables

Sign in

Forgot password?

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy

Password recovery

You can also try to

Or use a social network account

 

By Signing In \ Signing Up, you agree to our privacy policy