Table of contents
When creating a React application, the question of which state management approach to employ naturally arises. If your state is complex or includes a substantial number of entities, opting for Redux is advisable. However, if your state is relatively simple, the Context API might be a more suitable choice.
The second query is, since Redux allows for asynchronous operations, why is a Redux Saga still necessary?
Thus, let's go back to the beginning and contrast redux, redux toolkit, redux saga, and asynchronous operation in redux toolkit.
Redux :
Redux is a state management library used with JavaScript application often used with React. It offers a consistent state container that makes managing the state in a centralized area simple. Redux works on the principles of immutability and unidirectional data flow(can only pass data from parent to child). Reducers, which are pure functions, handle dispatching actions to update the state in Redux. Reducers accept the actions and, depending on the action type and payload, return a new state. Redux's lone drawback is that it requires creating a lot of boilerplate code, which could lengthen the development process.
Redux toolkit:
Redux Toolkit is basically a collection of tools and guidelines designed to make common Redux patterns easier to use. Pre configure store, which comes with Redux Toolkit, sets up reducers and middleware. Slices make it simple to build reducer functions and action creators. The asynchronous code can be worked with by using createAsyncThunk.
Redux saga:
Redux Saga is a middleware library for Redux that is used to manage side effects in your Redux application. When we talk about side effects in relation to Redux, we usually mean asynchronous tasks like managing timers, interacting with browser storage, or making API requests. Redux Saga offers a more methodical and testable approach to managing these side effects.
Redux Saga can handle numerous asynchronous operations concurrently and wait for each one to finish before moving on. It also has built-in support for cancelling and aborting asynchronous actions.
Redux and Redux Toolkit provide the core architecture for state management in an application. while Redux Saga focuses on declaratively handling asynchronous side effects. Particularly Redux Toolkit has several tools that can help with asynchronous code work more easily, but it falls short of Redux Saga in terms of declarative control over side effects.
As you can see redux saga placed between action and the reducers. so we can say that redux saga delays the execution of certain actions. Now let's understand redux saga implementation along with redux toolkit with an example.
In this example, we're building a small React application that features a list of cats. When a user clicks on a cat's image, they will be redirected to a detailed page for that specific cat. Additionally, within each cat card, clicking on the breed will trigger an action to retrieve and display a list of cats belonging to that particular breed.
To set up the project, we've initiated a Create React App and installed essential packages such as Redux Toolkit, React Router DOM, and React Saga. In the 'src' folder, a 'redux' directory has been created to house the core Redux files:
store.js: This file configures the Redux store, incorporating the slices and middleware required for the application.
catSlice.js: Within this file, the Redux slice for cat-related actions and state management is defined. It includes actions for fetching cat data and reducers to handle the state changes.
catSaga.js: The saga file contains the asynchronous logic using Redux Saga. It listens for specific actions, such as fetching cat data, and handles the side effects, such as API calls.
Redux saga uses the generator function. Generator function are special type of functions in JavaScript that can be paused and resumed during their execution. It uses the special keyword "yield". the generator function defined using function* functionName(). when this function is called it returns an iterator objects called as generator object.
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = myGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
generator is an object and we are calling next method on this object, the generator function is executed until it encounters a yield statement. The value property of the object returned by generator.next() contains the value yielded by the generator.
first we are going to configure our store where we include all reducers and saga middleware. The reducer key, is an object contains all our application reducers combined in a single global reducer. Here you can see the root saga function which includes two sagas. The all function is a utility that allows you to run multiple sagas concurrently. The middleware key represents the list of middleware(array) will be used in the redux configuration. finally we run root saga using middleware and exporting our store.
App.js
import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import catReducer from "./catSlice";
import catSaga, { singleCatSaga } from "./catSaga";
import { all } from "redux-saga/effects";
function* rootSaga() {
yield all([catSaga(), singleCatSaga()]);
}
const sagaMiddleware = createSagaMiddleware();
export const store = configureStore({
reducer: { cat: catReducer },
middleware: [sagaMiddleware],
});
sagaMiddleware.run(rootSaga);
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 and built-in middleware.
index.js
import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; import { Provider } from "react-redux"; import { store } from "./redux/store"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode> ); reportWebVitals();
The component Provider is a React element that is exported from the react-redux package. It accepts the store configuration as a prop named store and the App component as a child. The plan is for every component within the application to have access to the store.
Certainly! Below is an example of a
catSlice.js
file that defines the initial state, reducers for updating the state, and exports the reducer:
//catSlice.js
import { createSlice } from "@reduxjs/toolkit";
const catSlice = createSlice({
name: "Cats",
initialState: {
catsList: [],
loading: false,
catListByBreed: [],
selectedID: null,
catDetail: {},
},
reducers: {
fetchCats: (state) => {
state.loading = true;
},
getCats: (state, action) => {
state.loading = false;
state.catsList = action.payload;
},
getCatID: (state, action) => {
state.selectedID = action.payload;
},
setCatDetails: (state, action) => {
state.catListByBreed = action.payload;
},
setSingleCatDetail: (state, action) => {
state.catDetail = action.payload;
},
},
});
export default catSlice.reducer;
export const {
fetchCats,
getCats,
getCatID,
setCatDetails,
setSingleCatDetail,
} = catSlice.actions;
Reducers takes the state and action as a parameter and returns a new state. state CatList contains all the cats details when we loads or refresh the page. catListByBreed contains catList when we click particular BreedName. catDetail state is used for the single cat Detail. we are exporting the Reducer (whole reducer) ad import this reducer into store.js file.
In this example:
The initial state (initialState
) includes catList
for all cats, catListByBreed
to store cat lists by breed, and catDetail
for the details of a single cat.
The
createSlice
function from Redux Toolkit is used to define a slice of the Redux state, including the initial state and reducers.Three reducers (
setCatList
,setCatListByBreed
, andsetCatDetail
) are defined to update different parts of the state based on the dispatched actions.The exported
catSlice.reducer
is what you'll import into yourstore.js
file.The exported actions (
setCatList
,setCatListByBreed
,setCatDetail
) can be used in your components or sagas to dispatch actions to update the state.
Now you can import this reducer into your store.js
file and configure your Redux store with it.
Before moving to saga file let's try to understand about some effects which are mostly used in saga.
1. Call: used to call a function or making asynchronous API calls in saga
2. put: is used to dispatch a action within saga. It allows you to trigger additional actions in response to the completion of asynchronous operation.
yield.put({type:"ADD_TO_CART",payload:item}
yield.put(setCatDetails(catDetail))
takeEvery: is used to watch the occurrence of the specific action. It allows concurrent handling of multiple instance of specified actions.
takeLatest: it is similar to takeEvery but it handles the latest occurrence of the specified action. suppose if multiple actions dispatched then it only proceed the latest and previous instances are cancelled.
Two more hooks we are going to used in this application which are below.
useSelector : useSelector is a React hook provided by the react-redux library that allows components to read data from the Redux store.useDispatch : In Redux, useDispatch is a React hook provided by the react-redux library that allows components to dispatch actions to the Redux store.
//catSaga.js
catSaga.js file
import {
fetchCats,
getCats,
setCatDetails,
setSingleCatDetail,
} from "./catSlice";
import { call, put, take, takeEvery, takeLatest } from "redux-saga/effects";
const route = `https://api.thecatapi.com/v1/images/search?limit=30&api_key=${process.env.REACT_APP_API_KEY}`;
// Step 1: Dispatching the fetchCats action in the component
// Example usage: dispatch(fetchCats());
function* catSaga() {
yield takeLatest("Cats/fetchCats", watchingCatSaga);
}
// Step 2: watches for the specified action type ('FETCH_CATS') and triggers
//the watchingCatSaga worker saga and sets the response to the catLists State
function* watchingCatSaga() {
const allCats = yield call(() =>
fetch(
`https://api.thecatapi.com/v1/breeds?api_key=${process.env.REACT_APP_API_KEY}`
).then((res) => res.json())
);
const formattedCats = yield allCats.slice(0, 20);
yield put(getCats(formattedCats));
}
//Step:3 The singleCatDetails saga initiated when a breed name is clicked
function* singleCatSaga() {
yield takeLatest("Cats/getCatID", SingleCatDetails);
}
function* SingleCatDetails({ payload }) {
try {
const catDetailResponse = yield call(() =>
fetch(
`https://api.thecatapi.com/v1/images/search?breed_ids=${payload}&limit=20`
)
);
const catDetail = yield catDetailResponse.json();
yield put(setCatDetails(catDetail));
} catch (error) {
// Handle any errors here
console.error("Error fetching single cat details:", error);
}
}
export default catSaga;
export { singleCatSaga };
so let's discussed what we have done in our saga.js file
In this file "cats/fetchCats" is similar to {type:"FETCH_CATS"} string used for action name
1. First with the help of dispatch we call the fetchCats function in home page where It sets the loading state to true.
2. we created a catSaga which takes the action type and the callback function watchingCatSaga where we making the api call using call effects and simply get the response of cat data. using a put effect we dispatch an action which takes the catlist as payload. you can see that in getCats reducer function we set the action.payload data into catList state.
3. when we click the breed name on catCard component The singleCat
saga is initiated. It sets the selectedID state to the breed id and then calls the singleCatdetails
with the action type 'getCatID'.
//home.jsx
import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { fetchCats, getCatID, setSingleCatDetail } from "../redux/catSlice";
import { useNavigate } from "react-router-dom";
const Home = () => {
const dispatch = useDispatch();
const { catsList, loading } = useSelector((state) => state.cat);
const navigate = useNavigate();
useEffect(() => {
dispatch(fetchCats());
}, [dispatch]);
const cardClickHandler = (cat) => {
dispatch(getCatID(cat.id));
navigate(`/breed/${cat.id}`);
};
const clickHandler = (cat) => {
dispatch(setSingleCatDetail(cat));
navigate(`/cat/${cat.name}`);
};
return (
<>
<h1>Images of diffrent breed's cats</h1>
<div className="cat_container">
{loading ? (
<p>Loading.....It takes some time to render</p>
) : (
catsList?.map((cat) => (
<div key={cat.id} className="card">
<div onClick={() => clickHandler(cat)}>
<img src={cat?.image?.url} alt="cat photo" />
</div>
<div>
<div className="description">
<span> Name: {cat.name} ,</span>
<span>Origin: {cat.origin} ,</span>
<span>Temparement: {cat.temperament}</span>
<span onClick={() => cardClickHandler(cat)} className="link">
{" "}
Breed: {cat.id}
</span>
{/* <a href={cat.wikipedia_url}>Wikipedia_url</a> */}
</div>
<p className="paragraph">{cat.description}</p>
</div>
</div>
))
)}
</div>
</>
);
};
export default Home;
In home.jsx file, you can see how we dispatched our action for setting a particular cat detail in object.
- In this step, we don't require any extra saga or any generator function because we just set the single cat object by clicking on image of cat. so we just set that cat object using setSingleCatDetail reducer function.
conclusion
For simple application, using redux only might be sufficient but for more complex application or scenarios with asynchronous logic, Redux saga can be a powerful. I hope this article helpful to you to understand the redux saga with redux toolkit. If there is any confusion or you have any suggestions then please let me know in the comment section. Here is the live link and code link of example which we have discussed.
Happy coding !!!