Getting Started with Redux Saga Tutorial

A complete saga tutorial with redux basics. Learn redux saga by working on examples from basic to some advanced levels. Create a blog app with redux-saga concepts.

Chaudhry Talha 🇵🇸
38 min readSep 2, 2022
Photo by Sigmund on Unsplash

Background:

About 2 months ago I started learning & working on redux-saga and I only had some understanding of redux. It’s something that can get confusing but once you get a gist of it, it’s going to make your code cleaner and your app better.

In this article, I have tried sharing information using a different approach (Than I do usually) where I’m focused on creating multiple small apps to understand one or a bunch of concepts. I hope this article helps you understand the concepts in a practical manner.

How will we roll?

We’ll begin with a quick refresher of redux, which will be good enough to build an understanding of what redux is, and then quickly go into the understanding of different concepts of redux-saga with examples.

Installations:

The libraries you’ll need to install are:

yarn add @reduxjs/toolkit@1.8.3
yarn add react-redux@8.0.2
yarn add redux-saga@1.1.3
//Optional
yarn add axios@0.27.2

For practice create a new react or react-native (up to you) project and install the above packages. I’ve created mine named redux-saga-tutorial:

npx create-react-app redux-saga-tutorial

Redux Refresher:

Let’s quickly get a refresher on some of the concepts in redux. At the end of this section, there is a full-coded basic redux example as well.

Initial State / Global State:
An initial state is a plain JS object that has all the initial values of your app. You can also say it’s a global state where all the up-to-date values will be.

For example, if your whole app is about incrementing and decrementing a number then your initial state will be an object with just const initialState = { value: 0, name: '' }. If there are more things like there is some data that is yet to be loaded from an API so we can have the initial state const initialState = { allUserChats: [], allUsers: [], isLoggedIn: false....} so you can use this as the initial state of your app that has all the default values of the things you’ll need throughout your app.

The initial state will be kept in the store where all the latest updated values of the initial state will be. Think of the store as the single source of truth and we will be reading and updating values in the store that redux provides.

Action:
An action is a plain JS string. We usually declare them as const SOME_ACTION_NAME = 'domain/eventName'. These constants are the unique names of the actions like 'profile/updateName' will mean that counter increment action. For everything, there should be an action variable.

Think of actions as anything that can happen in your app. For example if your app calls an API there should be an action that calls that specific API, then another action for it’s success and another one in case API fails to retrieve data.

Action Creator:
We create an action string like const SOME_ACTION_NAME = 'domain/eventName' and then we create action creators that are a function through which we can pass data. Usually, an action creator has two things,type that is the name of the action which we can pass the variable we have created and the second thing is payload in which we send data. The name payload can be anything.

function someActionCreator(text) { 
return { type: SOME_ACTION_NAME, payload: text }
}

If let’s say we had an action named const NAME_UPDATED = 'profile/updateName' then its action creator will be something like:

function updateProfile(text) {
return { type: NAME_UPDATED, payload: text }
}

You can name payload and type as you like, this is usually a convention that most developers use.

Reducer:
A reducer is a function that receives two things:

  • Current state of the initialState
  • action object (basically an action creator)
const initialState = { name: '' }function myReducer(state = initialState, action) {
switch(action.type) {
case NAME_UPDATED: return {...state, name: action.payload};
default: return state;
}
}

We have a switch statement that looks for an NAME_UPDATED action which is probably going to be declared somewhere in a separate file usually like const NAME_UPDATED = 'profile/updateName';

Whenever an action is taken it’ll call this reducer and the reducer will look at which action is taken and update the initial state accordingly. We haven’t connected any of these elements.

You can see that the action we’re going to receive will have two things state and action and action is an object as we saw in our action creator section, and we’re accordingly using type and payload that we’re sending.

Provider / Connecting Redux to main application:
Let’s quickly see how we wrap our app inside a redux layer, which allows the store object to be accessible throughout the app. In the main file say index.js we’ll create an import Provider which is a component that makes the store available to any nested components that need to access the store.

import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import { myReducer } from '../reducers';const root = ReactDOM.createRoot(document.getElementById('root'));// const rootReducer = combineReducers({myFirstReducer}); //in case you have more than one reducer. Don't forget to import it from @reduxjs/toolkitconst store = configureStore({ reducer: myReducer });root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);

In the code above, we have wrapped our entire <App /> within the store that has only one reducer that has one action. Now if there is any dispatch in the app the Provider will catch it a provide it to the store. The store will then go to reducer and perform the function accordingly.

Dispatch:
How do we trigger an action? Right now, your actions are in one file and your reducer is in another. An action has to be triggered so that the redux knows to go to the reducer and take the right action. Hence comes dispatch which you can import from import { useDispatch } from 'react-redux'; and then create a variable of it for example const dispatch = useDispatch(); which we can use as a dispatcher of an action. Say we have a button and in the onPress of that button, we can do dispatch({ type: NAME_UPDATED, payload: input.text }); but as we created an action creator updateProfile earlier so we can call that:

import { useDispatch } from 'react-redux';function example() {
const myDispatch = useDispatch();
return (
<Button title='Update Name' onPress={() => {
myDispatch(updateProfile('input.text'))
} />
)
};

Please do note that actions can be taken either by a user i.e. button onPress event or also by the app. You can trigger an action from a response of an API and so on.

Selector:
A selector allows you to extract data from the redux store. Just like useDispatch the selector also has its hook known as useSelector.

import { useSelector } from 'react-redux';function example() {
const mySelector = useSelector((store) => store);
return (
<Text>{mySelector.name}</Text>
)
};

useSelector() also forces the view to re-render but only causes a re-render if the selector result appears to be different than the last result. It’ll return the whole store. Hence you can access things that you have in the initial state, but with the updated value.

Putting it all together!
Let’s see the above example combined into this boilerplate code:

https://codesandbox.io/embed/redux-basic-example-ftrvs0?fontsize=14&hidenavigation=1&theme=dark

In the code example above we have dispatched (useDispatch) the action from App.js as well as we’re consuming (use selector) it in App.js. The app simply updates the value name and you can see the view starts to read the latest value instantly.

This is a very short example of how redux works. You can use the same concept to build other actions like loading an API data into a state where instead of name you’ll use some other name for example allProductsData or allUser etc and it’ll not be of string type but instead it’ll be an object {} or array [] or an array of objects [{},{}] or anything. So, according to your case you create actions and handle them in reducers. In the case of an API call, you can refer to the firebase example I wrote some time ago on this tutorial.

For those who will find this refresher useful, you can also try to add a new page, navigate to that page and call only the selector and you’ll see that the value will be the updated one.

Redux Saga:

The easiest way to explain redux-saga, at least in my opinion is that redux-saga separates your business logic from your front-end. Saga is what you call an event-driven solution where you launch events and take care of the logic that is required to be executed on that particular event launch.

To understand how redux-saga works you can find a lot of flow diagrams that might get clear once you have some understanding of the concept of redux-saga. Here is a diagram to get a high-level idea:

In the above diagram when the user press the button Get Users, it dispatched an action. That action goes to Saga, then in the saga file, we have defined what to do when a certain action is taken. It can be called an API or other type of functionality we want our app to do when a certain action is taken. So the Calling API / Wait for Results stage is all a part of what functionality we want to do when a certain action is taken. The reason why this step is on dotted --- the pattern is that it’s not a mandatory step and the saga can directly go to the reducer but checking the reducer is a must-to-do step in the process and then the reducer, if there is a need, will update the state otherwise returns the same state. Here is a GIF image I found on the internet I believe it’s by developpaper website but it shows how an action to deposit $10 usually takes place in a redux-saga environment. Think of middleware in the GIF below as Saga.

With the above flow in mind, let’s start learning sagas.

To understand the concept of redux-saga a hello-world-like example would be to call an API. So, let’s write a program that will do just that using sagas, but before we do that there are two things related to JavaScript that you should be aware of to understand redux-saga:

I wrote a brief overview of generator functions which will cover yield part as well here:

So, I’m not going to get in-depth about Generator functions & yield in this tutorial, but to learn saga you will have to know the above JavaScript concepts.

Now let’s first set up the high-level things that will start our saga. Just like how we did in the index.js file for redux when we were setting up the store.

// ... Other importsimport { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from '@redux-saga/core';
import { myReducer } from "./reducers";
import App from "./App";
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({ reducer: myReducer, middleware: [sagaMiddleware] });// TODO: We need to run saga here, which we'll do shortlyroot.render(<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);

In the code above we did three things:

  1. We imported createSagaMiddleware which as specified in the name will help us in the creation of the saga in our app.
  2. We created a variable sagaMiddleware that will let us access all the things saga has to offer.
  3. We included our variable sagaMiddleware in our store so that it can be accessed throughout the app just like how we did for the reducer.

So, these three things will prepare saga so that it’s available throughout the app. I think of saga as a service running in the background constantly listening to messages and when a message is initiated it knows what to do next.

Let’s add these three actions in our actions.js

export const GET_USERS_FETCH = 'GET_USERS_FETCH';export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS'; //will be called when we finish the API call successfully
export const GET_USERS_FAILURE = 'GET_USERS_FAILURE'; //will be called when we finish the API call unsuccessfully

When we create action we also create an action creator in most cases. Here we only need an action creator for GET_USERS_FETCH as it’ll be called with dispatch and the useDispatch takes an object as a parameter and you will see shortly that when using the saga effects we can pass in the string so we won’t need action creators for that. So, let’s create that action creator in actionCreators.js

import { GET_USERS_FETCH } from "./actions";export function takeGetUserFetchAction() {
return { type: GET_USERS_FETCH };
}

A simple javascript object returning with just type as there are no parameters required, but in case you need to pass parameters refer to updateProfile(text) in the redux refresher section.

Now we’ll create a new file named sagas.js and we’ll build this file in three parts:

PART 1 — As our goal is to call an API using sagas. So, we’ll first add a normal function that calls the API and returns the response.

import axios from 'axios';function userFetch() {// In case you don't want  to use axios
//return.fetch('https://jsonplaceholder.typicode.com/users').then(response => response.json());
return axios.get('https://jsonplaceholder.typicode.com/users').then((res) => {return res.data;}).catch((err) => {throw err})}

In the above part, we have simply made a function that returns res.data; upon successful retrieval and throw err if there is an error.

PART 2 — Next we’ll write a generator function that will call the userFetch() function and trigger an action accordingly to the response.

import { call, put } from "redux-saga/effects";import { GET_USERS_SUCCESS, GET_USERS_FAILURE } from "./actions";// ... other imports like axios
// ... other code like userFetch function
function* getUsersFetch() {try {
const users = yield call(userFetch);
//yield will wait for this call to finish before proceeding to the next line.
yield put({ type: GET_USERS_SUCCESS, users });
} catch (error) {
yield put({ type: GET_USERS_FAILURE, error });
}
}

A very straight-forward generator function that call the userFetch and hence we have used yield it’s going to wait until a response or an error is returned from the userFetch. From redux-saga/effects we have call which is used to call the function and we’re storing the results in users variable. Next, put creates an Effect description that instructs the middleware to schedule the dispatching of action to the store. This dispatch may not be immediate since other tasks might lie ahead in the saga task queue or still be in progress. So, when the dispatch is finished it’ll pass the result to either success or failure.

Part 3 — As in a redux framework, actions play an important part as they are the unique processes that are running in your app each with a different purpose. So, we have one action left to fill and that is GET_USERS_FETCH which we’ll use to call the getUsersFetch every time we dispatch this action.

import { call, put, take } from 'redux-saga/effects';
import { GET_USERS_FETCH } from './actions'
// ... other imports like axios, actions etc
// ... other code like userFetch, getUsersFetch functions
function* mySaga() {
while (true) {
yield take(GET_USERS_FETCH);
yield call(getUsersFetch);
}
}
export default mySaga;

In the code above we’re saying that every time GET_USER_FETCH action is taken, the saga is going to call getUsersFetch generator function where we’re making the call to the API and triggering actions when it’s successful or failure.

take creates an Effect description that instructs the middleware to wait for a specified action on the Store. That means it’ll be listening when the button is pressed, as we dispatch an action, and because of take(GET_USERS_FETCH) saga knows or was listening to this action and as soon as this action is taken it’ll call the getUsersFetch generator function. So, yield is a very important concept to know, as it’s like a guard on the door that pauses/resumes a process accordingly. Meaning yield take(GET_USERS_FETCH) would be on-hold until the GET_USERS_FETCH action is taken. Once that action is taken, that yield is done and then yield call will work and it’ll also hold executing further until a response came from the getUsersFetch.

The name of this generator function is mySaga and this will be our main saga just like we saw in a root or main reducer and we can combine sagas using all but for this particular example, there will be only one.

That’s all for the sagas.js file.

Next, we’ll prepare our reducer.js file which will take the action.

import { GET_USERS_FAILURE, GET_USERS_SUCCESS } from "./actions";const initialState = {};const myReducer = (state = initialState, action) => {switch (action.type) {case GET_USERS_SUCCESS: return { ...state, payload: action.users };
case GET_USERS_FAILURE: return { ...state, payload: action.error };
default: return state;
}};export default myReducer;

The reducer will be called when there is a success or failure event happened. Just two things left to do now. One is to make changes App.js to make it ready to show the data that the API is going to return and the second is running the saga for which we added a // TODO: in index.js. Let’s see what it means by running the sagas first.

Think of sagas as a service constantly running in the background where you can toss in the number of actions the user has taken and it’ll properly manage to resolve and return your success or failure. It’ll wait, solve the current one and then take on the next action. So we need to run it in our root file. Open up index.js and add this one more line of code (actually 2):

import mySaga from './sagas';// ... all other import and code abovesagaMiddleware.run(mySaga);// ... other code like root.render(...

The run will start our saga to listen to actions being dispatched, as long as the app is running at least. So, next, we’ll do just that, we’ll open App.js and start making our UI where we’ll have a button that’ll trigger the fetch action.

import { useDispatch, useSelector } from "react-redux";
import { takeGetUserFetchAction } from "./actionCreators";
function App() {const myDispatch = useDispatch();
const retrivedData = useSelector((state) => {
return state.myReducer;
});
return (<div className="App">
<h1>Users</h1>
<button onClick={() => myDispatch(takeGetUserFetchAction())}>
Call API
</button>
<hr />
<div>
{retrivedData?.users && retrivedData.users.map((user) => ( <div key={user.id}>{user.name}</div>))}{retrivedData?.error && <p>{retrivedData.error.message}</p>}</div>
</div>);
}export default App;

In our, useSelector we could’ve returned the whole state but instead, we picked only the reducer we’re interested in. As we’ll have users or error in it so we are rendering our data accordingly. If you run it now and click on the button it’ll show you the data. But if you change the URL in the sagas file to say https://jsonplaceholder.typicode.com/user you’ll see the error printed out.

Our three cases i.e. GET_USERS_FETCH, GET_USERS_SUCCESS, GET_USERS_FAILURE

So, using saga we have made a simple API request. Here is the code that shows a running example for everything we have discussed above:

How to make the above example simpler

We have achieved our goal in the above example. But we can make it more simple by using some of the redux hooks that you’ll see will make things very easy to do. There are so many like createAction, createReducer etc that we’ll look at.

We took a basic approach to keep it as vanilla JS as possible so that you understand the behind-the-scenes as things can be easier. So, what we’re going to do now is to simplify the above example using some more hooks and effects and see how that simplifies things.

By now if you haven’t noticed how saga works you can think of saga as a service running in the background, constantly listening for actions and as soon as that action is triggered, saga takes the action and provide extended functionality to things that enable you to run different functions on a different thread and provide a fully asynchronous way of doing things, etc.

The first thing we need to do is instead of two files actions.js and actionCreators.js we’ll only need one i.e. actions.js and in it we’ll add this code:

import { createAction } from "@reduxjs/toolkit";export const GET_USERS_FETCH = createAction('GET_USERS_FETCH');
export const GET_USERS_SUCCESS = createAction('GET_USERS_SUCCESS');
export const GET_USERS_FAILURE = createAction('GET_USERS_FAILURE');

This has created action and action-creator for each of these, so this saves a lot of time and reduces the complexity of creating and using actions. You also don’t have to worry about where a String (action) or an Object (action creator) will go as when we’ll pass this to dispatch, selector, or reducer it’ll be used accordingly and automatically. Meaning we don’t have to worry about it where we need to pass an action or an action creator as createAction takes care of both and will return what is needed accordingly.

You can also pass parameters and do much more with createAction and see how simple it’ll make action creation for you.

Next, we’ll look at createReducer, which by name is clear that it’ll create a reducer for you saving you from some hassle. It takes two parameters createReducer(INITIAL_STATE, builder callback function) (a builder function is a function that you can join using a . and keep building the chain-like structure.)

So, we re-code our reducer.js file as:

import { createReducer } from "@reduxjs/toolkit";
import { GET_USERS_FAILURE, GET_USERS_SUCCESS } from "./actions";
const initialState = {};const myReducer = createReducer(initialState, (builder) => {builder.addCase(GET_USERS_SUCCESS, (state, action) => {
state.users = action.users;
}).addCase(GET_USERS_FAILURE, (state, action) => {
state.error = action.error;
}).addDefaultCase(() => {});
});export default myReducer;

In the code above, we did things similar to how we did in the switch statement but here instead of that, we have used a builder function approach. We can .addCase as many as we want and then use .addDefaultCase (optional) to handle the default statement hence building a chain-like builder.addCase(...).addCase(...).addCase(...)….addDefaultCase(...). Every case has a state and action which we can specify to update our state/initial state. Pretty straightforward if you think of it as a switch statement format.

So, you have seen how using createAction and createReducer can redux some complexity of the code. To make the above changes work there s just one small change that we have to do in App.js where we are dispatching the action, we’ll replace it as:

// ... other imports
import { GET_USERS_FETCH } from './actions';
// ... other code
<button onClick={() => dispatch(GET_USERS_FETCH())}>Call API</button>

As our GET_USERS_FETCH is created using createAction so it’ll automatically have an action creator so we can call it like GET_USERS_FETCH() and it’ll trigger the action, as since there were no changes in the saga, it’ll catch the action and the rest of the flow will work accordingly. Our reducer will also work as expected.

Since we’re extending our old example to make things easier, we should also cover another thing that is used widely in the apps using saga. Below is a brief note on all three of them and then we’ll see its use in the app we’re building.

  • take
    The result of yield take(pattern) is an action object being dispatched, It tells the middleware (saga) to wait for a specified action taken on the store. Take only takes an action once, so in order for it to take an action every time we click on a button we wrap it in a while(true) {...} or use takeEvery or takeLatest as per our use case.
  • takeEvery
    Enables calling the GET_USERS_FETCH actions at the same time. At a given moment, we can start a new GET_USERS_FETCH task while there are still one or more previous GET_USERS_FETCH tasks that have not yet been terminated. When we take GET_USERS_FETCH action, we dispatched it from a button. takeEvery allows concurrent actions to be handled. In the example above, when a GET_USERS_FETCH action is dispatched, a new GET_USERS_FETCH task is started even if a previous GET_USERS_FETCH is still pending (for example, the user clicks on a Call API button 2 consecutive times at a rapid rate, the 2nd click will dispatch a GET_USERS_FETCH action while the fetchUser fired on the first one hasn't yet terminated)
  • takeLatest
    Only one GET_USERS_FETCH task can be active at any given moment. It will also be the work that was started most recently. If a new GET_USERS_FETCH job is started while a previous task is still running, the previous work will be terminated immediately. On the contrary to takeEvery, takeLatest will stop any similarly running actions and will start a new one. Each time an action is dispatched to the store. And if this action matches pattern, takeLatest starts a new saga task in the background. If a saga task was started previously (on the last action dispatched before the actual action), and if this task is still running, the task will be canceled.

To see a practical example of take, takeEvery, and, takeLatest trying reading this:

So, open the sagas.js and replace the take and call line with:

import { call, put, takeEvery } from "redux-saga/effects";function* mySaga() {  yield takeEvery(GET_USERS_FETCH, getUsers);}

It’s a very straightforward way of saying for every GET_USERS_FETCH run getUsers function. You’ll see it works just the same as it was working before. Now the reason for using takeEvery and not takeLatest or take totally depends on your use case.

So, what you did basically is converted the same app you built previously, but this time we focused on making things simpler. Hence we learned the use of createAction, createReducer, takeEvery etc.

Here is the code with all the changes we just did:

Simplifying Advanced Concepts

We’ll cover some more concepts here that are considered advanced but are rather simple if you understand the essence of it and just like how we saw things getting simpler in the previous sections it’ll be the same case here. I can’t cover everything but I’ll take an example that will use as many concepts as possible. Some of the topics we’ll see are fork, cancel, createSelector, and some others.

It’ll be a good point to have a general understanding of which saga effects are going to block the flow and which are not going to block. This means whether if yield is going to wait (like async-await does) before executing the next line of code or not will depend on the effect we’re using. So, here is a brief to-the-point explanation:

Blocking/Non-Blocking effects

We have seen many effects of the saga like take, call, put, etc. We can classify them into two categories:

  • Blocking
  • Non-Blocking

A Blocking call means that the Saga yielded an Effect and will wait for the outcome of its execution before resuming the next instruction inside the yielding Generator.

A Non-blocking call means that the Saga will resume immediately after yielding the Effect. In other words, the caller starts the task and continues executing it without waiting for it to complete.

TLDR; Blocking will block the flow and non-blocking will not block the way. This means as we have learned that yield will pause the execution until a response/error is provided or it is resolved/rejected from the current action, but there are some saga effects that are non-blocking that don’t pause the process and while they execute their action functionality in the background they let the code to execute the next line. Here is an example:

There are many others and you might not use all of them in your app but it’s good to know which one is blocking or non-blocking calls. Here is a list of effects and if they are blocking/non-blocking:

From the blocking/non-blocking explanation you can drive a conclusion that by using sagas we’re controlling a lot about how logic will flow through our system and we’re controlling the data flow and each effect has its own features that are useful in different use cases.

Fork & Cancel

fork() is used to run tasks in parallel (kind of what takeEvery does) but it does not block the flow. It is useful when a saga needs to start a non-blocking task.

You can think of fork() as taking a process, and executing it separately on a separate thread you can say. When it’ll will resolve or reject the task, it’ll notify the function it was being executed from and hence the compiler will exit that function. This might sound familiar to many concepts but the key difference is that fork will not hold the compiler from executing the next line of code but it’ll hold the compiler from exiting the function it is inside. Meaning the compiler will only exit a function when all the fork are done with either resolve or response. Have a look at the example below to understand this concept practically.

The Example

In your app, assume a user is on a screen where they can do the following things:

  • Call API to getUsers
  • Call API to getPosts
  • Call API to getComments
  • Exit the app also means Going back to the previous screen

We’re only going to see Call API to getPosts first using saga’s fork.

All the actions are not dependent on each other, they are four separate expected actions on the same screen that users can take at any time.

You can think of an action as anything that a user can do on a screen in your app. For example if you have on a login screen some of the actions a user can take on that screen would be login/emailAdded (You can use this action to check if the email is correct otherwise prompt an error then and there), login/passwordAdded, login/ForgotPasswordSelected, login/LoginError, login/LoginSuccess, and many more things that can happen on that screen.

I have the following actions in actions.js

export const GET_POSTS_FETCH = createAction("GET_POSTS_FETCH");
export const GET_POSTS_SUCCESS = createAction("GET_POSTS_SUCCESS");
export const GENERAL_FAILURE = createAction("GENERAL_FAILURE");export const EXIT_APP = createAction("EXIT_APP");

Most of the actions created are self-explanatory, but the EXIT_APP is an action that I’ve added to demonstrate some of the effects we’re going to use. You can think of exit as when the user is exiting/going back from a screen etc, we call our exit function to clean up the sagas because you don’t want sagas to constantly listen to an action when you’re not expecting that action. Post fetch will be used when we want to call the API and get results. General failure will be used if there is an error thrown by the API call and success is going to be triggered when we successfully retrieved the data from the API.

Let’s start building our sagas.js which we’ll do in three parts:

// PART 1
function getPosts() {
console.log("Now calling getPosts API")return axios.get('https://jsonplaceholder.typicode.com/posts').then((res) => {
return res.data;
}).catch((err) => {
throw err
})
}

The above code is nothing but a simple JS function calling an API and returning the result. Next, we’ll add a saga that’ll

  1. Call getPosts API
  2. Fetch the response or error
import { call, put, take } from 'redux-saga/effects';// PART 2
function* getPostsSaga() {
while (true) {try {console.log("getPosts action ready...");yield take(GET_POSTS_FETCH);
console.log("getPosts action started...");
const posts = yield call(getPosts);
console.log("getPosts action fetched...");
yield put({type: GET_POSTS_SUCCESS, posts: posts});
console.log("getPosts action finished...");
} catch (error) {console.log("getPosts action failed...");
yield put({type: GENERAL_FAILURE, error: error});
console.log("getPosts action error finished...");
}}}

I’ve added console logs so that you can efficiently see where the code stops and where it’s waiting etc. The above code is taking the action GET_POSTS_FETCH and then calling the API and upon receiving a response or error running things accordingly.

Now, we need a main Saga to call the getPostsSaga we have just created. So, let’s create that:

import { call, put, take, fork, cancel } from 'redux-saga/effects';
// PART 3
export default function* mySaga() {
const posts = yield fork(getPostsSaga); console.log("Now waiting for action from user..."); yield take(EXIT_APP); console.log("Exiting App...");
yield cancel(posts);
console.log("Exit Finished...");
}

Remember as fork is a non-blocking call, so it doesn’t wait like how yield call(... or others like take wait for a response or error. So in the code above, we’re basically telling a story that as soon as the saga start we have forked our getPostsSaga, which means we’ve taken the function on a separate thread to execute it while the rest of the flow is going to run without waiting for the getPostsSaga function to finish.

As the first line of getPostsSaga is a take effect. So it’ll start “listening or observing” for the GET_POSTS_FETCH action until this action is taken i.e. take. If we didn’t have a blocking call i.e. take in getPostsSaga it would execute the full function, so we added take as we don’t know when this action will be taken but whenever the user will take this action, the saga will be waiting to execute it on a separate thread (meaning without interfering the current flow of the app).

Then it’ll go back to the mySaga and print the Now waiting for action from user… and that’s it for this file now. I’ll explain the yield take(EXIT_APP) part shortly. (Because of console.log’s it’ll become much clearer when we’ll run it.)

So we did three things in our sagas.js to achieve just the get posts from API functionality

  • Created a function getPosts to get the latest posts from the API
  • Created a generator function for the getPosts, which handles the response, error, and call to the API.
  • Created the main generator function for sagas to fork the getPostsSaga

That’s all we need to do in the sagas file for now. Next, we’ll set up the reducers.js

import { createReducer } from "@reduxjs/toolkit";import { GET_POSTS_SUCCESS, GENERAL_FAILURE } from './actions';const initialState = {};const myFirstReducer = createReducer(initialState, (builder) => {builder.addCase(GET_POSTS_SUCCESS, (state, action) => {state.posts = action.posts}).addCase(GENERAL_FAILURE, (state, action) => {state.error = action.error}).addDefaultCase(() => {})})export default myFirstReducer;

We only want to handle two actions as the others are just actions that are not going to be used by the app to take certain actions itself and user has no concerns with it meaning it’s not going to retrieve any data that we want to show to the user so that’s why just these two actions are added.

Lastly, in your screen from which you’ll test these i.e. App.js open that and add this:

{/* ...other code... */}<button onClick={() => myDispatch(GET_POSTS_FETCH())}> Posts API </button>{/* ...other code... */}

The button upon pressing it we want to take the action GET_POST_FETCH which our saga is listening to. So as soon as we take this action we’ll see getPosts the function will then execute as normal. Let’s run it first and see the result and then see what is happening:

As soon as the app loads we see that because of fork it went into the getPostsSaga which we already know means that it’s ready and whenever the GET_POST_FETCH action will be taken, saga will be ready to take that action by executing the rest of the getPostsSaga. Then it came back and prints that the app has forked the methods and is now waiting for the user to take any possible action anytime so far we have just two actions that a user can take:

  1. Get posts from API
  2. Exit app

Let’s first click on the Posts API button:

As soon as the action is taken we see that it’ll start take then call the API after we have the data fetched we’re going to finish. And since we’re using while(true) so it’s ready again if the action is taken. If you press the Posts API button again it’ll again call the API:

Now, what will happen if you press the exit button? Remember when we printed Now waiting for action from the user… right after we’re also listening for yield take(EXIT_APP) So, in total we’re listening for two actions one is to call the posts API which is a non-blocking call, and then an exit which is a blocking call. Saga will be listening to both of these and as soon as either of these actions is taken saga will continue the flow from there.

So if we click on the exit button now it’ll print the console.log we have

As soon as the exit is called we cancel(posts) which is telling saga to stop listening to GET_POST_FETCH action. So, if you press the Posts API button now it’ll not do anything. This is how we can control the flow in our app i.e. keeping things limited to the need. In order to reopen that action either the user will have to re-enter the screen or restart, so you have to plan the use of fork accordingly, as they are super useful and will make your life easier when used properly.

Extending our current example (The Blog App)

To learn concepts like createSelector, fork, and some others let’s extend the above example by adding two new buttons i.e. Comments API and UsersAPI which are going to do exactly the same i.e. call the API and retrieve the results just as we did with posts.

So start by adding those actions:

import { createAction } from "@reduxjs/toolkit";export const GET_POSTS_FETCH = createAction("GET_POSTS_FETCH");
export const GET_COMMENTS_FETCH = createAction("GET_COMMENTS_FETCH");
export const GET_USERS_FETCH = createAction("GET_USERS_FETCH");
export const GET_POSTS_SUCCESS = createAction("GET_POSTS_SUCCESS");
export const GET_COMMENTS_SUCCESS = createAction("GET_COMMENTS_SUCCESS");
export const GET_USERS_SUCCESS = createAction("GET_USERS_SUCCESS");
export const EXIT_APP = createAction("EXIT_APP");
export const GENERAL_FAILURE = createAction("GENERAL_FAILURE");

Now we have all the actions we need for this example. Now let’s update our reducer for the two new success cases i.e. GET_USERS_SUCCESS, GET_USERS_SUCCESS

import { createReducer } from "@reduxjs/toolkit";
import {
GET_USERS_SUCCESS,
GET_POSTS_SUCCESS,
GET_COMMENTS_SUCCESS,
GENERAL_FAILURE
} from "./actions";
const initialState = {};const myReducer = createReducer(initialState, (builder) => {builder.addCase(GET_POSTS_SUCCESS, (state, action) => {
state.posts = action.posts;
})
.addCase(GET_COMMENTS_SUCCESS, (state, action) => {
state.comments = action.comments;
})
.addCase(GET_USERS_SUCCESS, (state, action) => {
state.users = action.users;
})
.addCase(GENERAL_FAILURE, (state, action) => {
state.error = action.error;
})
.addDefaultCase(() => {});
});
export default myReducer;

Next, we want to add what is going to happen when the user wants to fetch comments or user data. So let’s write whose API call functions in our sagas.js file as that’s where we are handling our business logic.

function getComments() {console.log("Now calling getComments API");
return axios.get("https://jsonplaceholder.typicode.com/comments").then((res) => {
return res.data;
}).catch((err) => {
throw err;
})
}
function getUsers() {console.log("Now calling getUsers API");
return axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
return res.data;
}).catch((err) => {
throw err;
})
}

Just like getPosts we added two new API call functions. Now, we need to add sagas for both, which basically means to listen to when an action is taken and execute and return the results accordingly. Similar to getPostsSaga we’ll add getCommentsSaga and getUsersSaga.

If you remove the console.log function is very straightforward. Because of take it’s going to start listening to this action. As soon as that action is taken our saga is going to call the API function and upon success, it’ll call the success action and put the data in the reducer. The reducer will add it to our global state i.e. initialState.

We have to bind our two new added sagas with our main saga that is running at the root level i.e. mySaga

import { call, cancel, fork, put, take, all } from 'redux-saga/effects';//... other codeexport default function* mySaga() {const posts = yield fork(getPostsSaga);
const comments = yield fork(getCommentsSaga);
const users = yield fork(getUsersSaga);
console.log("Now waiting for action from user...");yield take(EXIT_APP); // keep listening until the action is takenconsole.log("Exiting App...");yield all([cancel(posts),
cancel(comments),
cancel(users),
]);console.log("Exit Finished...");
}

We used fork to tell sagas to listen to all of these three. As fork is non-blocking so it’ll register all three plus EXIT_APP and then wait for any of the four actions to be taken. fork basically went inside the getCommentsSaga and getUsersSaga generator functions and since we have the blocking take in them so that’s how the saga has registered four actions for listening i.e. GET_POSTS_FETCH, GET_COMMENTS_FETCH, GET_USERS_FETCH, and EXIT_APP. Note that the success actions like GET_COMMENTS_SUCCESS etc are not yet registered and if let's say they are taken nothing is going to happen. When EXIT_APP will be taken we are also canceling the comments and users listeners.

Now we need to call the fetch actions from a button click so update the App.js or index.js to:

//... other imports
import {
GET_POSTS_FETCH,
GET_COMMENTS_FETCH,
GET_USERS_FETCH,
EXIT_APP
} from "./actions";//... other UI<button onClick={() => myDispatch(GET_POSTS_FETCH())}>
Posts API
</button>{" "}
<button onClick={() => myDispatch(GET_COMMENTS_FETCH())}>
Comments API
</button>{" "}
<button onClick={() => myDispatch(GET_USERS_FETCH())}>Users API</button>{" "}
<button onClick={() => myDispatch(EXIT_APP())}>Exit App</button>
<hr />//... other UI after rendering posts
{retrivedData?.comments &&
{retrivedData?.comments &&retrivedData.comments.map((comment) => (
<div key={comment.id}>
<p style={{ fontWeight: "bold" }}>{comment.name}</p>
<p style={{ fontSize: 12, marginTop: -16, marginBottom: -10 }}>
{comment.email}
</p>
<p>{comment.body}</p>
</div>
))}
{retrivedData?.users &&retrivedData.users.map((user) => (
<div key={user.id}>
<p style={{ fontWeight: "bold" }}>
{user.name} ({user.company.name})
</p>
<p style={{ fontSize: 12, marginTop: -16, marginBottom: -10 }}>
{user.email} | {user.phone}
</p>
<p>
{user.address.suite} {user.address.street}, {user.address.city}, {user.address.zipcode}
</p>
</div>
))}

The above code is going to print the retrieved data. The best way to see this is to run the app.

As soon as you run, you can see our program is listening to all three actions and a user can take any of the three actions. Let’s first take the Users API action.

Users API fetched the data and we can see it getting rendered in a nice format. Now, let’s take other actions, you can take with one post, or get or even users again as you can see it’s ready again because of the while(true) we have used. I’m going to take Comments API next:

Comments are fetched and if you scroll all the way down you can see your user's data available as well. As in UI users, data is rendered after the comments.

So far, we have just created two more saga functions just the same way we did in the previous section. The code till that is below:

Let’s look at some more hooks, basically, we are ready to start giving our example of the shape of a blog app.

createSelector

Now, we’ll see how we can use a convenient hook named createSelector. To use it install this package:

yarn add reselect *OR* npm i reselect

As we saw in our redux refresher section, a selector is used to select values from an object. Selectors are simply functions that are used to select a subset of data from a larger data collection.

In the case of this example, we have things like comments, users, and posts and we can use the selector to slice data so that we can select comments by user id 1 or select posts by the user with id 5, etc. Basically, a blog where we have fetched all the data and now we’re displaying it. In an ideal scenario, there will be APIs to get say specific posts for a user, and then from there, you can create a selector to say show the top 5 posts of the user and so on. Based on your scenario you use a selector and for the sake of this example we’ve fetched all the comments and users and posts and now we’re going to create as many selectors as we want to basically shape our data accordingly to how should happen in a blog application.

In our blog app, here is how we’ll do things:

  1. At the start of the app, we’ll trigger all three fetch API actions so that we have all the data. (Not a good approach in the real world but doing it just for the sake of this tutorial)
  2. Then we’ll display a list of posts only to the user, which will be clickable.
  3. Users can select any post. Upon selecting a post we’ll trigger another action that’ll get comments and user information for that particular user. (This is where we’ll use the createSelector)
  4. Go back to all posts.

Displaying the list of Blog Posts:

Let’s do the first thing and that is to fetch all the data we need. We’re going to remove the buttons like Posts API, Comments API, etc. As these actions will be triggered on the launch of the app. So, in App.js add this:

import { useEffect } from "react";//... Other importsfunction App() {   const myDispatch = useDispatch();   useEffect(() => {
// Get all the data at start. As we'll use selector to slice and show data.
myDispatch(GET_POSTS_FETCH());
myDispatch(GET_COMMENTS_FETCH());
myDispatch(GET_USERS_FETCH());
}, [myDispatch]);//... Other code

In the code above we’ve dispatched the three Actions to get all the data. The reducer and the saga will remain the same and we know that we have the data in retrivedData which is a selector we created in the previous example.

Just to do a bit clean up let’s move our getPosts, getComments, and getComments function from sagas.js to create a new file apis.js and paste them into that. Basically, we’re only keeping the generator functions in the saga file.

// apis.js
import axios from "axios";
export async function getPosts() {console.log("Now calling getPosts API");return await axios.get("https://jsonplaceholder.typicode.com/posts").then((res) => {
return res.data;
}).catch((err) => {
return err;
});
}export async function getComments() {console.log("Now calling getComments API");return await axios.get("https://jsonplaceholder.typicode.com/comments").then((res) => {
return res.data;
}).catch((err) => {
return err;
});
}export async function getUsers() {console.log("Now calling getUsers API");return await axios.get("https://jsonplaceholder.typicode.com/users").then((res) => {
return res.data;
}).catch((err) => {
throw err;
});
}

In the apis.js file above, we have added the three get API functions and removed them from sagas.js file.

Now, let’s make changes to our UI (App.js). Remove everything inside the main return (…) and add this code:

return (
<div className="App">
<h3>The Blog App</h3>
<div>
<p>
<b>Select a blog post from blow to read more:</b>
</p>
<hr />
{retrivedData?.posts && retrivedData.posts.map((post) => (
<a href={`/#${post.id}`} onClick={() => {
console.log(post);
}} key={post.id}>
{post.id}: {post.title}
</a>
<br />
))}
</div>
</div>
)

This UI just has a link <a>...</a>. The href is just added to give it a link-like feel, run the app and select a post. You’ll see it gets printed in the console.

So, our logic works. Just by doing these changes, we have finished the first two things out of the total 4 that we discussed above.

Number 3, is where when the user will select any of these posts we need to display that post, its comments, and its author. As we have all the data already, all we need is to filter and display the appropriate one. For example, if the user selects post number 31, we need to get comments for the post with id 31 and the user name who wrote that post. That’s where we’ll create selectors. Selectors, as it’s all in the name, select the data. So let’s create a new file and name it selectors.js and in it add our first selectors.

export const allPosts = (state) => state.myReducer.posts;const allComments = (state) => state.myReducer.comments;
const allUsers = (state) => state.myReducer.users;

In the code above we have simply exported our allPosts that we have retrieved from our global state, as that’s the only thing we need to export because we’re displaying a list of all the posts. The other two allComments and allUsers are just there to get all the data and next we’re going to make selectors that we need.

If you look at the documentation of createSelector you’ll see it takes three parameters but the required are the first two as shown below:

In the code we did, we have three data objects that we can use as our input selectors. Input selector means the data you want to select from. The resultFunc is a function that you can use to slice and return the data in any way you want. Note that there can be multiple input selectors (we’ll see shortly) which means you can mix and match data just by creating a selector for it.

Our goal is that when a user will select a post we need to get the selected post, its comments, and its author information. ← In order for this to happen a user has to select which means the user has to take an ACTION.

So in your actions.js add this action:

//... other actions
// Action which takes post as parameter
export const SELECTED_POST = createAction("SELECTED_POST", (post) => {return {
payload: {
selectedPost: post
}
};
});

This is the first time we have created an action using createAction that accepts a parameter. This means when we’ll dispatch this action we’ll pass it an object post and I have created an object { payload: { selectedPost: post }}; (You can structure it as you like).

We need to store this post object in our global state so that we can access it in our selector and other places. So, let’s go to the reducers.js and add this case to our createReducer.

//... Other imports import {
GET_USERS_SUCCESS,
GET_POSTS_SUCCESS,
GET_COMMENTS_SUCCESS,
GENERAL_FAILURE,
SELECTED_POST
} from "./actions";
//... other ccode.addCase(SELECTED_POST, (state, action) => {state.selectedPost = action.payload.selectedPost;})//... other code

So I'm storing it as selectedPost in my global state. Now, let’s get back to our selectors. We’re ready to create a selector in which we’ll select the comments of a selected post. Open selectors.js and add this code:

import { createSelector } from "reselect";//...other codeexport const selectedPost = (state) => state.myReducer.selectedPost;export const getCommentsForPost = createSelector(= createSelector(allComments, selectedPost,(c, p) => {if (c && p) {
const filteredComments = c.filter((comment) => {
return comment.postId === p.id;
});
return filteredComments;
};
});

In the code above, we have imported our createSelector and then just like how we did for allPosts etc we have fetched the selectedPost from our global state. Now for our getCommentsForPost we have provided allComments and selectedPost as input selectors. (This is how you can provide multiple input selectors.) Then I’ve added a function i.e. resultFunc that has c which represents an object from allComments and p which is the selected post-data object. Then by using vanilla javascript code I’ve used filter to filter out comments where postId === p.id.

We need one more selector and that is to get the user name which we’ll display as the author of the selected post. So, let’s create a selector in which we’ll select the author of the selected blog post:

export const getAuthorForPost = createSelector(
allUsers, selectedPost,
(u, p) => {
if (u && p) {
const author = u.find((user) => {
return p.userId === user.id;
});
return author;
}});

Same as before we have added two input selectors and using a result function we’re looking for the object in allUsers that matches the p.userId.

Now that we have our selectors set up, it’s time to see them in action. All that’s left now is to paint our UI to display all the event changes. So, open up App.js and make the following modifications:

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import {
GET_POSTS_FETCH,
GET_COMMENTS_FETCH,
GET_USERS_FETCH,
SELECTED_POST,
EXIT_APP
} from "./actions";
import {
allPosts,
selectedPost,
getCommentsForPost,
getAuthorForPost
} from "./selectors";
//... other code function App() {...const retrivedPosts = useSelector(allPosts);
const selPost = useSelector(selectedPost);
const selPostComments = useSelector(getCommentsForPost);
const selPostAuthor = useSelector(getAuthorForPost);
// This useState is just to hide/show UI based on post selection
const [selectedPostModeOn, setSelectedPostModeOn] = useState(false);
function postSelected(selectedPost) {
myDispatch(SELECTED_POST(selectedPost));
}
//... More code

In the first set of changes to our UI, we first imported all the actions that were needed and all the selectors we need. Since I just have one view so I’ll hide show UI elements as per the selected mode i.e. when a post is selected and when it’s not so I’ve added a useState to control that. There is a function postSelected that gets called when a post is selected and it just dispatches the SELECTED_POST action along with the parameter selectedPost object and the rest is already been taken care of.

Next, in our return(... of App.js we’ll have two divs:

  • The first div will have all the posts displayed, as we already have.
  • Second div will show the selected post, its comments, and the author's name.

At any given time only one of the above divs will be visible which we’ll control by using selectedPostModeOn.

Inside the main div i.e. <div className=”App”> add this code:

{selectedPostModeOn && (<div>{/* Back Button */}<button
onClick={() => {
myDispatch(EXIT_APP());
setSelectedPostModeOn(false);
}}> Back </button>
<br /> <hr />{/* Post */}<div> <code>Post Id: {selPost ? selPost.id : "No post selected"}</code>
<h1>{selPost.title}</h1>
<p>{selPost.body}</p>
<p>
<i>Written By: {selPostAuthor.name}</i>
</p>
</div> <hr />
{/* Comments */}<p style={{ fontSize: 12, fontWeight: "bold" }}> Comments ({selPostComments.length}):</p>{selPostComments && selPostComments.map((comment) => (<div key={comment.id} style={{
backgroundColor: "#e8e8e8",
padding: 8,
marginBottom: 8,
borderRadius: 8
}}>
<p style={{ fontSize: 12, color: "#7d7d7d" }}>
{comment.name} <br /> {comment.email}
</p>
<p style={{ fontSize: 12, fontWeight: "bold" }}>
{comment.body}
</p>
</div>))}</div>)}

The above component is a div that shows the post content and the author's name and finally all the comments.

The rest of the return remains the same but just wrap the div that display all posts in !selectedPostModeOn like:

{!selectedPostModeOn && (<div><p>
<b>Select a blog post from blow to read more:</b>
</p>
<hr />{retrivedPosts && retrivedPosts.map((post) => (<a href={`/#${post.id}`} onClick={() => {
postSelected(post);
setSelectedPostModeOn(true);
}}
key={post.id}>
{post.id}: {post.title}<br /></a>))}</div>)}

Same thing but two minor changes. First, we’re now rendering them using retrivedPosts as I removed retrivedData because it was no longer needed.

Now if you run the app it should work like this:

Complete working code of the blog app is available:

Conclusion:

Just like the sample project you did in this tutorial, depending on the project you can take advantage of redux-saga and make your project in a more event-driven architecture. You can plan things like all the actions you’ll be needing, and all the functionality that your sagas will handle. If you need to store something in your global state you need to handle that in a reducer and we have seen ways of simplifying redux-saga by using built-in hooks like createReducer, createSelector as well as effects from saga like call, put, take, fork, and some others. There are many other effects like select which is used when you want to call a selector in sagas. Instead of using useSelector sagas has select(SELECTOR_NAME) which we didn’t need in this example. Also race, spawn and many more which you can now explore and see what is the use of them and if you can make your saga more efficient by using any of those hooks.

As always I’ll leave you with things you can do to test the knowledge you have just gained:

  • Extend the blog app (go to its code sandbox and fork it) and extend it so that the written by: … is clickable and upon clicking it shows the user profile (all user data) and the posts written by the user. Try to make the Ui looks like a social media profile of a user.
  • Give this a read https://redux-saga.js.org/docs/advanced/RacingEffects/ and see if you can extend the blog to use some of the effects given in the article.

If you find this helpful share and press the 👏🏻 button so that others can find it too. If you see a typo or something that is wrong feel free to highlight it and I’ll update it with credits or if you’re stuck drop a comment and I’ll try my best to help you.

buymeacoffee.com/chaudhrytalha

All my tutorials are free and if you feel like supporting you can buymeacoffee.com/chaudhrytalha

Happy Coding 👨🏻‍💻

--

--