How to implement Redux Saga with React redux-toolkit

Najm Eddine Zaga
13 min readMar 11, 2023

--

How to implement redux-toolkit in a React application with Redux Saga.

React + Redux Toolkit + Redux Saga all using hooks

When developing a React application you start asking yourself about what to use for some features. But, when it comes to “storing data”, it’s even “Context API” or “Redux”.

So, you should ask yourself some questions…

  • Do I have an API to consume ?
  • Am I consuming too much data from this API ?
  • Is there too many entities in this API ?

If your answers are “Yes” then you’re going to use“Redux”. Otherwise, you’ll use “Context API”.

Note: You can use Context API when you have all “yeses”, and you can use the “useReducer” hook provided by React, but I don’t think it’s going to be as organized as Redux structure when the application gets bigger.

1 — Overview

A — What is Redux ?

Redux is a state management library for JavaScript applications, often used with React. It provides a predictable state container, which allows for easy management of application state in a single, centralized location. Redux is based on the principles of immutability and unidirectional data flow, which make it easier to reason about changes to the application state over time. With Redux, actions are dispatched to update the state, and these actions are handled by pure functions called reducers, which return the new state based on the action type and payload. Redux also provides middleware for handling asynchronous actions and other advanced use cases. Overall, Redux helps to simplify the management of complex application state and makes it easier to build scalable, maintainable applications.

Therefor… We’re using Redux Toolkit.

B— What is Redux Toolkit ?

Redux Toolkit includes a pre-configured Redux store, which includes the necessary middleware and reducer setup, as well as support for DevTools. It also includes a set of powerful utilities for creating Redux slices, which are small, self-contained modules of Redux state and logic. With slices, you can define reducer functions and action creators in a more concise and intuitive syntax. Overall, Redux Toolkit provides a streamlined and opinionated way to use Redux, which can help developers write better and more maintainable Redux code with less effort.

C — Asynchronous operations

Managing Redux state is a simple task, but consuming a REST API should be considered within the Redux structure.

Redux Toolkit also provides a set of utility functions for working with asynchronous code, including createAsyncThunk for defining Redux thunks and createEntityAdapter for managing normalized data in Redux.

D — Redux Thunk Vs. Redux Saga

Both Redux Thunk and Redux Saga are middleware libraries for Redux that provide an approach for managing asynchronous logic in Redux applications.

  • Redux Thunk is a simple middleware library that allows you to write action creators that return a function instead of an action object. This function can then perform asynchronous operations, such as API calls, and dispatch actions once the operation is complete. The function can also dispatch additional actions based on the result of the operation. This approach is simple to learn and can be used for most common use cases. Redux Thunk is easy to set up and can be used with minimal configuration.
  • Redux Saga, on the other hand, is a more powerful and flexible middleware library for managing asynchronous operations in Redux. It allows you to define complex, asynchronous logic as a sequence of steps, called sagas. Sagas can be used to handle more complex scenarios, such as race conditions, retries, and cancellation. Redux Saga uses generator functions to define sagas, which allows for more complex logic to be expressed in a more readable and intuitive way. However, it can be more difficult to learn and use effectively compared to Redux Thunk.

Overall, Redux Thunk is a simpler and easier-to-use middleware library for managing asynchronous operations in Redux, while Redux Saga provides a more powerful and flexible approach for handling more complex scenarios. The choice between them largely depends on the complexity of the application and the specific use cases that need to be handled.

As mentioned before, in this article, I’ll show you … step by step … how to implement Redux Saga with Redux Toolkit. All Saga used functions will be explained in a simple way.

Note: I’ll provide Typescript code snippet. If you’re using Javascript, just remove types.

2 — Start coding

A — Create the React project

Let’s create a React project named redux-with-saga as an example:

# Javascript project
$ npx create-react-app redux-with-saga

# Typescript project
$ npx create-react-app redux-with-saga --template typescript

B — add packages

We’re adding the following packages:

$ npm install react-redux @reduxjs/toolkit redux-saga
# Or
$ yarn add react-redux @reduxjs/toolkit redux-saga

C — configure Redux store

Once the packages installation is complete, inside the src/ folder, create a new folder “store” or “redux” or anythink that refers to redux implementation:

Inside the “redux” folder, create an index.ts/js file to configure redux:

import createSagaMiddleware from "@redux-saga/core";
import { configureStore } from "@reduxjs/toolkit";

const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
reducer: {},
middleware: [sagaMiddleware],
});

export default store;

Explanation:

  • createSagaMiddleware — Creates a Redux middleware instance and connects the Sagas to the Redux Store.
  • configureStore — A function provided by the @reduxjs/toolkit package that simplifies the process of creating a Redux store with sane defaults and built-in middleware.

The reducer key, is an object contains all our application reducers combined in a single global reducer (object). For now we’re passing an empty object because we have no reducers yet.

The middleware key represents the list of middlewares will be used in the redux configuration.

And finally, we export the store.

D — Use store configration inside our React app

import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import store from './redux';

const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);

The component Provider is a React element exported from react-redux package that takes the App component as child and takes the store configuration as prop named store.

The idea is that the store will be accessible in every component inside the application. But… what if we move the Provider element inside the App component ? In this case, all components can use the redux store except the App component. Because it’s not considered one of the “children” for the Provider component. Easy as that…

E — Create root reducer

In general, we’ve done configuring our redux store, but… we don’t want our redux/index.ts/js to get bigger as we add more reducers. So… we’re creating a separate file named redux/root-reducer.ts/js:

import userReducer from "./users/slice";

export type StateType = {
// Reducers types here
};

const rootReducers = {
// Reducers here
};

export default rootReducers;

Creating a separate file contains the root reducer which is an object takes all our application reducers combined in a single reducer, is considered as best practice to keep things clean and clear.

The StateType is a Typescript type represents the type of our global state.

We go back to our index.ts/js file and pass the root reducer to the reducer key in the configuration:

import createSagaMiddleware from "@redux-saga/core";
import { configureStore } from "@reduxjs/toolkit";
import rootReducers from "./root-reducers";

const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
// Update the line below
reducer: rootReducers,
middleware: [sagaMiddleware],
});

export default store;

F — Create slice

Let’s consider we’re consuming some REST API’s users endpoints. To do that we’re creating 3 files, because I always want to keep things clean.

  • types.ts/js
  • slice.ts/js
  • sagas.ts/js

types file

We’re defining all types that can be used for the user’s entity across the project.

// Define the user type
export type UserType = {
id: string;
name: string;
lastname: string;
email: string;
active: boolean;
createdAt: Date;
updatedAt: Date;
}

// This type will represent the sub-state for getting a single user by ID
export type IUserState = {
data: UserType | null;
isLoading: boolean;
errors: string;
}

// The users global state
export type UsersStateType = {
user: IUserState,
// Later, we can add other sub-states like:
// list,
// create,
// update,
// remove
}

// (1)
export const USERS = "users";
export type USERS = typeof USERS; // Typescript line

// (2)
export const GET_USER_BY_ID = `${USERS}/getUserAction`;
export type GET_USER_BY_ID = typeof GET_USER_BY_ID; // Typescript line

(1) — creating a USERS constant that takes “users” text, then create a type with same name as the USERS const and give it the type of the const USERS. This will tell typescript: Anything that is typed with the type USERS, should accept only a value “users”. And if we assign the text “users2”, typescript will show you an error that the variable only accepts “users” as value.

This technique should make your coding safer as your project gets bigger, and you also can mistype the word “users” like “usres”. So, typescript will give you a heads up.

Same thing for (2) — GET_USER_BY_ID

Note: In TypeScript, typeof can be used as a type operator to extract the type of a value or variable as a type.

slice file

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { USERS, UsersStateType, UserType } from "./types";

const usersInitialState: UsersStateType = {
user: {
data: null,
isLoading: false,
errors: '',
}
}

export const usersSlice = createSlice({
name: USERS,
initialState: usersInitialState,
reducers: {
/* This action will trigger our saga middleware
and set the loader to true and reset error message.
*/
getUserAction: (state: UsersStateType, { payload: id }: PayloadAction<string>) => {
state.user.isLoading = true;
state.user.errors = '';
},
getUserSuccessAction: (state: UsersStateType, { payload: user }: PayloadAction<UserType>) => {
state.user.isLoading = false;
state.user.data = user;
},
getUserErrorAction: (state: UsersStateType, { payload: error }: PayloadAction<string>) => {
state.user.isLoading = false;
state.user.errors = error;
},
}
}

/* getUserSuccessAction and getUserErrorAction will be used inside the saga
middleware. Only getUserAction will be used in a React component.
*/

export {
getUserAction,
getUserSuccessAction,
getUserErrorAction
} = usersSlice.actions;
export default usersSlice.reducer;

After exporting the users reducer, we should add it in our root-reducer file and update the state type also:

import userReducer from "./users/slice";
import { UsersStateType } from "./users/types";
import usersReducer from "./users/slice";

export type StateType = {
users: UsersStateType;
};

const rootReducers = {
users: usersReducer,
};

export default rootReducers;

sagas file

import { PayloadAction } from "@reduxjs/toolkit";
import { AxiosResponse } from "axios";
import { put, takeLatest } from "redux-saga/effects";
import { UserType, GET_USER_BY_ID } from "./types";
import { getUserErrorAction, getUserSuccessAction } from "./slice";

// Generator function
function* getUserSaga({ payload: id }: PayloadAction<string>) {
try {
// You can also export the axios call as a function.
const response: AxiosResponse<UserType> = yield axios.get(`your-server-url:port/api/users/${id}`);
yield put(getUserSuccessAction(response.data));
} catch (error) {
yield put(getUserErrorAction(error));
}
}

// Generator function
export function* watchGetUser() {
yield takeLatest(GET_USER_BY_ID, getUserSaga);
}

What’s a generator function ?

In JavaScript, a generator function is a special type of function that can be paused and resumed during execution. When a generator function is called, it returns an iterator object, which can be used to control the execution of the function.

Generator functions are defined using the function* syntax, which is distinct from regular function syntax. Within a generator function, the yield keyword is used to pause execution and return a value to the caller. When the generator function is resumed, it continues executing from where it left off.

Here is an example of a simple generator function:

function* myGenerator() {
yield 1;
yield 2;
yield 3;
}

const gen = myGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

Read more about generator functions here.

yield — The yield keyword is a fundamental part of Redux Saga, as it allows the generator functions that Saga uses to pause and resume execution in response to dispatched actions. When a generator function encounters a yield statement, it returns an iterator that can be used to retrieve the value that was yielded. The value can then be processed by other parts of the Saga middleware.

In Redux Saga, yield is often used in combination with other functions, such as call, put, and select. For example, the call function is used to call asynchronous functions and return their results.

put — The put function is used to dispatch actions to the Redux store.

takeLetst — In Redux, takeLatest is a function provided by the redux-saga middleware that allows developers to handle asynchronous actions in a more controlled way. When an action is dispatched to the Redux store, takeLatest will cancel any previous ongoing tasks and only run the latest one.

To use takeLatest, another generator function is defined that will listen for a particular action.

In the code above, watchGetUser is a generator function that listens for the GET_USER_BY_ID action using takeLatest. When this action is dispatched, the getUserSaga generator function is called. getUserSaga calls an API and dispatches either a getUserSuccessAction or getUserErrorAction action using the put effect, depending on whether the API call succeeds or fails.

The benefit of using takeLatest is that it ensures that only the latest request is processed, even if previous requests are still in progress. This can help avoid race conditions and ensure that the application state is always in sync with the latest data.

Now we can add the watchGetUser generator function to the redux process, therefor … we’re creating a new file in the same level as root-reducer file named root-sagas.ts/js:

import { all, fork } from "redux-saga/effects";
import { watchGetUser } from "./users/sagas";

const rootSaga = function* () {
yield all([
fork(watchGetUser),
// Other forks
]);
};

export default rootSaga;

After we export the rootSaga, we should add it to the redux configuration file. So, in our index.ts/js:

import createSagaMiddleware from "@redux-saga/core";
import { configureStore } from "@reduxjs/toolkit";
import rootReducers from "./root-reducers";
// Add the import
import rootSaga from "./root-sagas";

const sagaMiddleware = createSagaMiddleware();

const store = configureStore({
reducer: rootReducers,
middleware: [sagaMiddleware],
});

// Added line
sagaMiddleware.run(rootSaga);

export default store;

Going back to the root-sagas file all the way down to the updates made in the index file:

all / fork — In Redux Saga, the fork and all functions are used to create and manage concurrent Sagas.

fork is used to create a new child Saga that runs concurrently with the parent Saga.

The all function is used to run multiple Sagas in parallel and wait for all of them to complete before continuing execution of the parent Saga.

If we have another saga like:

import { all, fork } from "redux-saga/effects";
import { watchGetUser } from "./users/sagas";

const rootSaga = function* () {
yield all([
fork(watchGetUser),
// Considering we have the watcher below
// fork(watchGetUsersList),
]);
};

export default rootSaga;

watchGetUser and watchGetUsersList are two Sagas that are started using all. The all function waits for both Sagas to complete before continuing the execution of the parent Saga rootSaga.

By using fork and all, we can create and manage concurrent Sagas in Redux Saga. This can help to make the application more responsive and efficient, as multiple tasks can be executed in parallel.

run — In Redux Saga, the run function is used to start the middleware and run the root Saga. The run function takes in a single argument, which is the root Saga function. The rootSaga function is passed to the run function of the middleware instance, which starts the Saga and allows it to listen for dispatched actions.

When the middleware is started using the run function, the root Saga is executed and any generator functions within it are started. The middleware then listens for dispatched actions and passes them to the relevant Saga generator functions using the yield keyword.

If a dispatched action matches a pattern defined within a Saga, the middleware will pause the Saga at the yield statement and run the code within the Saga. Once the Saga has completed its execution, the middleware will resume the generator function and continue processing any remaining dispatched actions.

G — use redux action in React component

After creating Redux configuration alongside with saga middleware, types, slice and sagas files. Now, it’s time to consume state and dispatch the trigger action.

Let’s consider we have a component which represents profile page.

Note: When writing this article I’m still convinced that you have good and solid knowledge in React.

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { getUserAction } from "../redux/users/slice";
import { StateType } from "../redux/root-reducer";

const ProfilePage: React.FC = () => {

const { data, isLoading } = useSelector((state: StateType) => state.users);

const { id } = useParams();

const dispatch = useDispatch();

useEffect(() => {
dispatch(getUserAction(id));
}, [id]);

return (
<div>
{
isLoading
?
(<span>Loading...</span>)
:
data
?
(<div>Hi, I'm {data.name}</div>)
:
(<span>No user found!</span>)
}
</div>
)
}

useSelectoruseSelector is a React hook provided by the react-redux library that allows components to read data from the Redux store. It takes a function as an argument, which should return the part of the store that the component is interested in.

When the store changes, useSelector will compare the previous value of the selected data to the new value, and trigger a re-render of the component if they are different. This makes it easy to keep the component in sync with the store without having to manually subscribe to store updates or manage state in the component.

Note that useSelector should only be used to read data from the store, and not to dispatch actions or modify the store directly.

useDispatch — In Redux, useDispatch is a React hook provided by the react-redux library that allows components to dispatch actions to the Redux store. It provides a way to trigger changes to the store from within a component. it is used to get a reference to the dispatch function provided by the Redux store. The component can then call dispatch with an action object to trigger a change to the store.

Note that useDispatch should only be used to dispatch actions, and not to read data from the store.

Using useDispatch and useSelector together is a common pattern in Redux applications, as it allows components to both read data from the store and dispatch actions to modify it.

Conclusion

Back in time, we used to store data with Redux library. With redux-toolkit, it’s now more easier and less code than it used to be. You can always find your own way to create a redux structure, like splitting slices into smaller slices to prevent large files and so on.

I hope you guys enjoyed reading this article and I hope it was helpful and full of “simplified and explained concepts”. If there’s anything still blurry or not clear enough, or you have any suggestions… please let me know in the comment section.

Happy coding!

References:

--

--

Najm Eddine Zaga
Najm Eddine Zaga

Written by Najm Eddine Zaga

🇹🇳 | Software Developer & Javascript enthusiastic | TypeScript | ReactJs | NextJs | NestJs | ExpressJs | Redux