Redux Saga for Efficient State Management in React
Kawan Idrees
Posted on July 28, 2024
In modern web development, managing application state can become increasingly complex as applications grow. Redux, a popular state management library, helps in managing state, but handling asynchronous operations such as API calls can still be challenging. This is where Redux Saga comes into play. In this blog, we will explore Redux Saga, its advanced concepts, and a practical example of implementing CRUD operations with JSON Server and React.
What is Redux Saga?
Redux Saga is a library that aims to make application side effects (like data fetching and impure computations) easier to manage, more efficient to execute, and better at handling failures. It uses ES6 generators to make asynchronous flows easy to read, write, and test.
Key Concepts in Redux Saga
1-Effects:
- call: Used to call asynchronous functions.
- put: Dispatches an action to the Redux store.
- take: Pauses the saga until an action is dispatched.
- fork: Creates a non-blocking task.
- race: Runs multiple effects concurrently and cancels all when one completes.
2-Advanced Effects:
- takeLatest: Takes the latest action if multiple actions are dispatched.
- takeEvery: Takes every dispatched action.
- debounce: Delays execution until after wait time has elapsed since the last time it was invoked.
- throttle: Ensures a function is called at most once in a given period.
Setting Up a Redux Saga Project
Step 1: Project Initialization
First, create a new React project and install the necessary dependencies:
npx create-react-app redux-saga-crud
cd redux-saga-crud
npm install redux react-redux redux-saga axios json-server
Step 2: Setting Up JSON Server
Create a db.json file to mock a backend server:
{
"users": [
{ "id": 1, "name": "John Doe", "email": "john@example.com" },
{ "id": 2, "name": "Jane Doe", "email": "jane@example.com" }
]
}
Start the JSON Server:
json-server --watch db.json --port 5000
Step 3: Redux Setup
Create the Redux store and root reducer:
// src/redux/store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);
export default store;
Create the root reducer and user reducer:
// src/redux/reducers/index.js
import { combineReducers } from 'redux';
import userReducer from './userReducer';
export default combineReducers({
users: userReducer
});
// src/redux/reducers/userReducer.js
import {
FETCH_USERS_SUCCESS,
ADD_USER_SUCCESS,
UPDATE_USER_SUCCESS,
DELETE_USER_SUCCESS
} from '../types/userTypes';
const initialState = {
users: []
};
const userReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_USERS_SUCCESS:
return { ...state, users: action.payload };
case ADD_USER_SUCCESS:
return { ...state, users: [...state.users, action.payload] };
case UPDATE_USER_SUCCESS:
return {
...state,
users: state.users.map(user =>
user.id === action.payload.id ? action.payload : user
)
};
case DELETE_USER_SUCCESS:
return {
...state,
users: state.users.filter(user => user.id !== action.payload)
};
default:
return state;
}
};
export default userReducer;
Step 4: Redux Saga Setup
Create action types and action creators:
// src/redux/types/userTypes.js
export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
export const ADD_USER_REQUEST = 'ADD_USER_REQUEST';
export const ADD_USER_SUCCESS = 'ADD_USER_SUCCESS';
export const ADD_USER_FAILURE = 'ADD_USER_FAILURE';
export const UPDATE_USER_REQUEST = 'UPDATE_USER_REQUEST';
export const UPDATE_USER_SUCCESS = 'UPDATE_USER_SUCCESS';
export const UPDATE_USER_FAILURE = 'UPDATE_USER_FAILURE';
export const DELETE_USER_REQUEST = 'DELETE_USER_REQUEST';
export const DELETE_USER_SUCCESS = 'DELETE_USER_SUCCESS';
export const DELETE_USER_FAILURE = 'DELETE_USER_FAILURE';
// src/redux/actions/userActions.js
import {
FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE,
ADD_USER_REQUEST, ADD_USER_SUCCESS, ADD_USER_FAILURE,
UPDATE_USER_REQUEST, UPDATE_USER_SUCCESS, UPDATE_USER_FAILURE,
DELETE_USER_REQUEST, DELETE_USER_SUCCESS, DELETE_USER_FAILURE
} from '../types/userTypes';
export const fetchUsersRequest = () => ({ type: FETCH_USERS_REQUEST });
export const fetchUsersSuccess = users => ({ type: FETCH_USERS_SUCCESS, payload: users });
export const fetchUsersFailure = error => ({ type: FETCH_USERS_FAILURE, payload: error });
export const addUserRequest = user => ({ type: ADD_USER_REQUEST, payload: user });
export const addUserSuccess = user => ({ type: ADD_USER_SUCCESS, payload: user });
export const addUserFailure = error => ({ type: ADD_USER_FAILURE, payload: error });
export const updateUserRequest = user => ({ type: UPDATE_USER_REQUEST, payload: user });
export const updateUserSuccess = user => ({ type: UPDATE_USER_SUCCESS, payload: user });
export const updateUserFailure = error => ({ type: UPDATE_USER_FAILURE, payload: error });
export const deleteUserRequest = id => ({ type: DELETE_USER_REQUEST, payload: id });
export const deleteUserSuccess = id => ({ type: DELETE_USER_SUCCESS, payload: id });
export const deleteUserFailure = error => ({ type: DELETE_USER_FAILURE, payload: error });
Create the user saga:
// src/redux/sagas/userSaga.js
import { takeLatest, call, put, all, fork } from 'redux-saga/effects';
import axios from 'axios';
import {
FETCH_USERS_REQUEST, FETCH_USERS_SUCCESS, FETCH_USERS_FAILURE,
ADD_USER_REQUEST, ADD_USER_SUCCESS, ADD_USER_FAILURE,
UPDATE_USER_REQUEST, UPDATE_USER_SUCCESS, UPDATE_USER_FAILURE,
DELETE_USER_REQUEST, DELETE_USER_SUCCESS, DELETE_USER_FAILURE
} from '../types/userTypes';
const apiUrl = 'http://localhost:5000/users';
function* fetchUsers() {
try {
const response = yield call(axios.get, apiUrl);
yield put({ type: FETCH_USERS_SUCCESS, payload: response.data });
} catch (error) {
yield put({ type: FETCH_USERS_FAILURE, payload: error.message });
}
}
function* addUser(action) {
try {
const response = yield call(axios.post, apiUrl, action.payload);
yield put({ type: ADD_USER_SUCCESS, payload: response.data });
} catch (error) {
yield put({ type: ADD_USER_FAILURE, payload: error.message });
}
}
function* updateUser(action) {
try {
const response = yield call(axios.put, `${apiUrl}/${action.payload.id}`, action.payload);
yield put({ type: UPDATE_USER_SUCCESS, payload: response.data });
} catch (error) {
yield put({ type: UPDATE_USER_FAILURE, payload: error.message });
}
}
function* deleteUser(action) {
try {
yield call(axios.delete, `${apiUrl}/${action.payload}`);
yield put({ type: DELETE_USER_SUCCESS, payload: action.payload });
} catch (error) {
yield put({ type: DELETE_USER_FAILURE, payload: error.message });
}
}
function* watchFetchUsers() {
yield takeLatest(FETCH_USERS_REQUEST, fetchUsers);
}
function* watchAddUser() {
yield takeLatest(ADD_USER_REQUEST, addUser);
}
function* watchUpdateUser() {
yield takeLatest(UPDATE_USER_REQUEST, updateUser);
}
function* watchDeleteUser() {
yield takeLatest(DELETE_USER_REQUEST, deleteUser);
}
export default function* rootSaga() {
yield all([
fork(watchFetchUsers),
fork(watchAddUser),
fork(watchUpdateUser),
fork(watchDeleteUser)
]);
}
Combine the sagas:
// src/redux/sagas/index.js
import { all } from 'redux-saga/effects';
import userSaga from './userSaga';
export default function* rootSaga() {
yield all([
userSaga()
]);
}
Step 5: Integrating with React Components
Create the Users component to list, add, update, and delete users:
// src/components/Users.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsersRequest, deleteUserRequest } from '../redux/actions/userActions';
import { Link } from 'react-router-dom';
const Users = () => {
const dispatch = useDispatch();
const users = useSelector(state => state.users.users);
useEffect(() => {
dispatch(fetchUsersRequest());
}, [dispatch]);
const handleDelete = (id) => {
dispatch(deleteUserRequest(id));
};
return (
<div>
<h2>Users</h2>
<Link to="/add-user">Add User</Link>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user.email})
<Link to={`/edit-user/${user.id}`}>Edit</Link>
<button onClick={() => handleDelete(user.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default Users;
Create the AddUser component for adding a new user:
// src/components/AddUser.js
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addUserRequest } from '../redux/actions/userActions';
import { useHistory } from 'react-router-dom';
const AddUser = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const dispatch = useDispatch();
const history = useHistory();
const handleSubmit = (e) => {
e.preventDefault();
dispatch(addUserRequest({ name, email }));
history.push('/');
};
return (
<div>
<h2>Add User</h2>
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label>Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<button type="submit">Add User</button>
</form>
</div>
);
};
export default AddUser;
Create the EditUser component for updating an existing user:
// src/components/EditUser.js
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { updateUserRequest } from '../redux/actions/userActions';
import { useParams, useHistory } from 'react-router-dom';
const EditUser = () => {
const { id } = useParams();
const dispatch = useDispatch();
const history = useHistory();
const user = useSelector(state =>
state.users.users.find(user => user.id === parseInt(id))
);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
useEffect(() => {
if (user) {
setName(user.name);
setEmail(user.email);
}
}, [user]);
const handleSubmit = (e) => {
e.preventDefault();
dispatch(updateUserRequest({ id: parseInt(id), name, email }));
history.push('/');
};
return (
<div>
<h2>Edit User</h2>
<form onSubmit={handleSubmit}>
<div>
<label>Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div>
<label>Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<button type="submit">Update User</button>
</form>
</div>
);
};
export default EditUser;
Final Step: Routing in the App Component
Ensure you have routing set up in your App component:
// src/App.js
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from './redux/store';
import Users from './components/Users';
const AddUser = lazy(() => import('./components/AddUser'));
const EditUser = lazy(() => import('./components/EditUser'));
const App = () => (
<Provider store={store}>
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Users} />
<Route path="/add-user" component={AddUser} />
<Route path="/edit-user/:id" component={EditUser} />
</Switch>
</Suspense>
</Router>
</Provider>
);
export default App;
Conclusion
In this blog, we've delved into Redux Saga and how it simplifies managing asynchronous operations in Redux. We implemented a practical example of a CRUD application with JSON Server and React, showcasing how Redux Saga handles side effects, concurrent API requests, and more.
Redux Saga's power lies in its ability to manage complex asynchronous flows cleanly and predictably. Understanding these advanced concepts will help you build scalable and maintainable applications. Happy coding!
Posted on July 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 27, 2024