How Redux-Saga with React-Redux works
Let’s see the following diagram to understand better:
Redux-Saga and React-Redux work together to manage asynchronous operations (like API calls) and side effects (like data manipulation) in React applications.
When a React component dispatches an action (e.g., “FETCH_DATA”) to the Redux store, it passes through Redux middleware, including Redux-Saga. Redux-Saga triggers the corresponding saga function if a saga is “watching” for that specific action type.
This saga function, written using generators, manages the asynchronous flow. It might make an API call using libraries like Axios or fetch. The saga can also perform other actions, like waiting for promises to resolve or dispatching new actions based on conditions.
Once the saga finishes its work (e.g., receiving data from the API), it might dispatch a new action to update the Redux store state. React components connected to the store using React-Redux will detect this state change and re-render themselves with the updated data, reflecting the results of the asynchronous operation in the UI.
Let’s jump into the implementation.
First, let’s create a new React project by running the following command:
npm create vite@latest react-redux-saga-example -- --template react-ts
Installing Packages
After creating a React project, let’s configure the project structure. Let’s first install React-Redux, Redux Toolkit, and Redux Saga. We can install these packages by running the following command:
npm install react-redux @reduxjs/toolkit redux-saga
Let’s also install other necessary packages which will be required in the future:
npm install axios react-router-dom
Setup
After we install the necessary packages, let’s implement our project structure like the following:
Here, we can see some folders. Let’s discuss one by one:
- Constants: Contains some constant variables that can be used in multiple files and components.
- lib: Contains useful functions and libraries.
- pages: Contains different page components that will be rendered based on route URL.
-
store: All the logic for
react-redux
andredux-saga
goes into the store folder. - types: Contains necessary types based on project domain and scope.
[Note:* This project can be found in this SlackBlitz repo. So be sure to check that out.]*
Understanding Redux Saga and React Redux
Now, let’s first create store/index.ts
and paste the following code:
import createSagaMiddleware from '@redux-saga/core';
import { configureStore } from '@reduxjs/toolkit';
import rootReducers from './root-reducer';
import rootSaga from './root-saga';
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({
reducer: rootReducers,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(sagaMiddleware),
});
sagaMiddleware.run(rootSaga);
export default store;
This file configures the store to work with Reducer and integrate with redux-saga middleware. Don’t worry about the rootReducers
and rootSaga
for now, we will touch them eventually.
Let’s now create root-reducer.ts
inside the same store
folder, and paste the following code:
import usersReducer from './slices/users.slice';
import { UsersStateType } from '../types/user.types';
export type StateType = {
users: UsersStateType;
};
const rootReducers = {
users: usersReducer,
};
export default rootReducers;
In this code, we combine all the reducers to pass them into the store configuration.
Let’s now create the user reducer in the store/slices/users.slice.ts
file and paste the following code:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { USERS, UsersStateType, UserType } from '../../types/user.types';
const usersInitialState: UsersStateType = {
user: {
data: null,
isLoading: false,
errors: '' as unknown,
},
list: {
data: [],
isLoading: false,
errors: '' as unknown,
},
};
export const usersSlice = createSlice({
name: USERS,
initialState: usersInitialState,
reducers: {
getUserAction: (
state: UsersStateType,
{ payload: _ }: 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<unknown>
) => {
state.user.isLoading = false;
state.user.errors = error;
},
getUserListAction: (state: UsersStateType) => {
state.list.isLoading = true;
state.list.errors = '';
},
getUserListSuccessAction: (
state: UsersStateType,
{ payload: list }: PayloadAction<UserType[]>
) => {
state.list.isLoading = false;
state.list.data = list;
},
getUserListErrorAction: (
state: UsersStateType,
{ payload: error }: PayloadAction<unknown>
) => {
state.list.isLoading = false;
state.list.errors = error;
},
},
});
export default usersSlice.reducer;
Here, we are implementing usersSlice
using the createSlice
method from the redux-toolkit
. We are defining all the possible actions and returning the user reducer created from the usersSlice
. Then, we pass the userReducer
to the root reducer object.
Let’s now create our user saga to get the resources from the API. Let’s create store/sagas/users.saga.ts
and paste the following code:
import { PayloadAction } from '@reduxjs/toolkit';
import { AxiosResponse } from 'axios';
import { put, takeLatest } from 'redux-saga/effects';
import { usersSlice } from '../slices/users.slice';
import apiClient from '../../lib/apiClient';
import { ApiEndpoints } from '../../constants/api';
import {
GET_USER_BY_ID,
GET_USER_LIST,
UserType,
} from '../../types/user.types';
function* getUserSaga({ payload: id }: PayloadAction<string>) {
try {
const response: AxiosResponse<UserType> = yield apiClient.get(
`${ApiEndpoints.USERS}/${id}`
);
yield put(usersSlice.actions.getUserSuccessAction(response.data));
} catch (error) {
yield put(usersSlice.actions.getUserErrorAction(error as string));
}
}
function* getUserListSaga() {
try {
const response: AxiosResponse<UserType[]> = yield apiClient.get(
`${ApiEndpoints.USERS}`
);
yield put(usersSlice.actions.getUserListSuccessAction(response.data));
} catch (error) {
yield put(usersSlice.actions.getUserListErrorAction(error as string));
}
}
export function* watchGetUser() {
yield takeLatest(GET_USER_BY_ID, getUserSaga);
yield takeLatest(GET_USER_LIST, getUserListSaga);
}
Here, we fetch the API responses using the generator function and yield the returned objects to the redux-saga middleware.
Let’s see the API responses in action
First, let’s add the react-redux provider in our root component:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App.tsx';
import store from './store/index.ts';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Now, let’s see the below component how we can show the response in our component:
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { StateType } from '../store/root-reducer';
import { usersSlice } from '../store/slices/users.slice';
import { Link } from 'react-router-dom';
import { PageRoutes } from '../constants/page-routes';
function Users() {
const { list } = useSelector((state: StateType) => state.users);
const dispatch = useDispatch();
useEffect(() => {
dispatch(usersSlice.actions.getUserListAction());
}, []);
return (
<div>
{list.isLoading ? (
<span>Loading...</span>
) : list.data && list.data.length > 0 ? (
<div>
{list.data.map((user) => (
<div key={user.id}>
<Link to={`${PageRoutes.USERS.HOME}/${user.id}`}>
{user.name}
</Link>
</div>
))}
</div>
) : (
<span>No users found!</span>
)}
</div>
);
}
export default Users;
It will render the following display in our browser:
That’s all in this article! I hope you enjoy it.
Have a great day!
Top comments (0)