Recently, in my Internship, I have been shifted to a new project where the state management is in the redux toolkit and redux-saga is being used for asynchronous calls in react application. So I tried to use it in a small application to understand how both of these can be used together, with some documentation and two youtube videos I got an idea of how to use them together. In this blog, I will try to pen down my learning and will also attach the links to the youtube video at the end from where I have understood this mixture, enough intro let's begin.
WHAT IS REDUX-SAGA?
Redux-Saga is an intuitive Redux side effect manager, which means it's basically used for running side effects(asynchronous calls, etc) in a redux application. We know that redux doesn't support asynchronous calls on its own and for this, we used redux-thunk middleware. Redux-Saga is a kind of replacement for thunk which can be used to do the asynchronous task within a redux application, for simplicity we can consider redux-saga as a middleware library for redux.
WHAT IS REDUX-TOOLKIT?
Redux-Toolkit is a new, easy to set up, less boilerplate way to use redux. The major problem for using redux are:-
- Configuring a Redux store is too complicated
- I have to add a lot of packages to get Redux to do anything useful
- Redux requires too much boilerplate code
Redux Toolkit solves these problems.
before starting I expect you to have basic knowledge of the redux toolkit and redux-saga I will try to give a short description of the concepts but if you won't get the most out of this blog have a basic knowledge or toolkit and saga
Enough theory now let's get into code and understand how things work:-
GETTING STARTED
First of all, we need to install some packages for using redux-toolkit, redux-saga, and react-redux in our application.
You can create a react application with the redux template for create-react-app, open your terminal and write this command, it will setup basic redux application with redux-toolkit
npx create-react-app tookit-saga --template redux
we can also add redux-toolkit in an existing application as it is available as a package too:-
npm install @reduxjs/toolkit
Now we also need to install two more packages redux-saga and react-redux:-
npm install redux-saga
and
npm install react-redux
install these packages in your existing project and you are ready to rumble.
SETUP
Before Setting Up let me tell you what we are going to build, so we will be creating a simple app that will fetch data from an API and display it on or UI.
Now let's start setting up the project:-
in the src folder create a file I like it to name store.js
you can name it anything you want, basically, in this file, we are going to set up our global store for a redux application.
in your src/store.js
file write these code
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})
redux-toolkit gives us configureStore
function to set up or redux global store and helps us to get rid of quite boilerplate code (creating combined reducers then adding it to the store).
(to understand how legacy redux is different from redux-toolkit and how to migrate to redux-toolkit you can refer to this official documentation)
Now, we wrap our <App />
in or src/index.js
file with <Provider />
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { store } from './store'
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
This will allow the global store accessible to our whole application.
Now, let's create a slice from the global state, create a folder name features
in src
inside the features
folder create another folder for example it will be Posts
inside it we will create a file I will name it postSlice.js
.
Inside src/features/Post/postSlice.js
we will create a slice of our global state
import { createSlice } from "@reduxjs/toolkit";
const initialState = {};
export const postSlice = createSlice({
name: "posts",
initialState,
reducers: { }
});
export default postSlice.reducer;
After creating the slice we will add its reducer to the store.
import { configureStore } from "@reduxjs/toolkit";
import postReducer from "./features/post/postSlice";
export const store = configureStore({
reducer: {
posts: postReducer
},
});
Now our basic redux-toolkit is set up it's time to add redux-saga into our app
ADDING REDUX-SAGA
For adding saga we will first create a new folder where all sagas will go let's call it saga
, we have to modify our store too, remember I told you to consider redux-saga as a middleware library so we need to add them into the store and then we will add our reducers into our slice(you can see we have put an empty object for reducers in src/features/Post/postSlice.js
.
before creating saga let us add reducers into postSlice
, we will add two reducers, go to src/features/Post/postSlice.js
:-
import { createSlice } from "@reduxjs/toolkit";
const initialState = {};
export const postSlice = createSlice({
name: "posts",
initialState,
reducers: {
//will let you know why we created this reducer an empty function
getPosts: () => {},
setPosts: (state, action) => {
state.posts = action.payload;
}
}
});
export const {
getPosts,
setPosts,
} = postSlice.actions;
export default postSlice.reducer;
when we add reducers in our slice ActionCreators are automatically created thanks to redux-toolkit and we export it
Now create a src/request
folder inside which we will have a file that will contain all async requests, in our app we are using JSONplaceholder API for dummy data
inside src/request/request.js
create a function that will get the posts from the API (remember to install Axios)
import axios from "axios";
export const fetchPosts = async () => {
try {
const data = await axios.get("jsonplaceholder.typicode.com/posts");
return data;
} catch (error) {
console.log(error);
}
};
Let's create our saga now, in src/saga
create two files named post.saga.js
and rootsaga.js
first, let's create src/saga/post.saga.js
:-
import { put, call } from "redux-saga/effects";
import { fetchPosts } from "../requests/requests";
import { setPosts } from "../features/Post/postSlice";
export function* getPostData() {
try {
const { data } = yield call(fetchPosts);
yield put(setPosts(data));
} catch (error) {
console.log(error);
}
}
inside src/saga/rootsaga.js
we will create our watcherSaga:-
import { takeEvery } from "redux-saga/effects";
import { getPosts } from "../features/post/postSlice";
import { getPostData } from "./post.saga";
export function* watcherSaga() {
yield takeEvery(getPosts.type, getPostData);
}
there's a lot of new stuff here I will give you a short explanation of everything:-
- What are these functions with
*
?
These functions are called generator functions, A generator function is the same as a normal function, but whenever it needs to generate a value it uses the 'yield' keyword rather than 'return'. The 'yield' keyword halts the function execution and sends a value back to the caller, you can read about this here
- What is
watcherSaga
?
Watcher is a generator function watching for every action that we are executing. In response to that action, the watcher will call a worker function.
- What are
put, call, takeEvery
?
call
:- Creates an Effect description that instructs the middleware to call the function fn with args as arguments, basically it will call the function which we pass to it as an argument.
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, in simple words it dispatches the action, and according to the dispatched action corresponding reducer will run.
takeEvery
:- Spawns a saga on each action dispatched to the Store that matches the pattern, in simple words the first argument will be the action, and whenever the action is dispatched the corresponding function passed as the second argument is called.
These are very small descriptions, you can learn more about this in the official doucmentation
Now as everything is setup for the saga now modify our store to make our app working
now modify src/store.js
to make everything working:-
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import postReducer from "./features/post/postSlice";
import { watcherSaga } from "./saga/rootsaga";
const sagaMiddleware = createSagaMiddleware();
export const store = configureStore({
reducer: {
posts: postReducer
},
middleware: [...getDefaultMiddleware({ thunk: false }), sagaMiddleware]
});
sagaMiddleware.run(watcherSaga);
createSagaMiddleware()
:- Creates a Redux middleware and connects the Sagas to the Redux Store.
-getDefaultMiddleware()
:- getDefaultMiddleware accepts an options object that allows customizing each middleware in two ways: Each middleware can be excluded from inclusion in the array by passing false for its corresponding field.
-sagaMiddleware.run(watcherSaga)
:- it will dynamically run the saga in the background and will keep running the watcherSaga
Now everything is set up and we are ready to add our UI.
ADDING THE UI
In src/App.js
we will add our UI:-
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
import { getPosts } from "./features/Post/postSlice";
export default function App() {
const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.posts);
useEffect(() => {
dispatch(getPosts());
}, [dispatch]);
return (
<div className="App">
<h1>Toolkit Saga with POSTS</h1>
{!posts && <h1>Loading...</h1>}
{posts && posts.map((post) => (
<div
key={post.id}
style={{
border: "1px solid black",
margin: "1rem",
padding: "1rem"
}}
>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
}
useSelector and useDispatch
are the hooks provided by react-redux to consume and update the Redux store respectively.
THE FLOW
Let's understand the flow,
First, the store is created and saga middleware starts running and watcherSaga starts looking for the given action.
We dispatch an action from UI, then watcherSaga will run and the takeEvery
function will look whether the action dispatched is the same which is being there in the function if it is the same then it will call the saga generator function which will call the asynchronous function using the call
function, then we call put
function which will dispatch the action and will update the store
, and then the updated state will trigger re-render and the UI will be updated.
you can see the app on codesandbox here
Remember I told I will tell you why I have added an empty reducer in the slice and created another reducer that is updating the store, the reason for this is very logical,
if we would have the same action for both dispatches from UI and from the put()
function then it will again call the watcherSaga and then again the saga function will run and this will become an infinite loop, you can check it on your own too, in saga/post.saga.js
replace the existing function by this
import { put, call } from "redux-saga/effects";
import { fetchPosts } from "../requests/requests";
import { getPosts } from "../features/post/postSlice";
export function* getPostData() {
try {
const { data } = yield call(fetchPosts);
console.log(data)
yield put(getPosts(data));
} catch (error) {
console.log(error, "ss");
}
}
now run the app again and see the console you can see what I am saying.
This is it, I hope it will help you in some way and your feedbacks are really important to do give feedback.
resources I have followed:-
- https://youtu.be/2hZhIoRuua4
- https://youtu.be/9MMSRn5NoFY
- Official Redux saga docs
- Redux Toolkit docs
until next time