Redux Saga for Efficient State Management in React

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 challeng…


This content originally appeared on DEV Community and was authored by Kawan Idrees

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:

  1. call: Used to call asynchronous functions.
  2. put: Dispatches an action to the Redux store.
  3. take: Pauses the saga until an action is dispatched.
  4. fork: Creates a non-blocking task.
  5. race: Runs multiple effects concurrently and cancels all when one completes.

2-Advanced Effects:

  1. takeLatest: Takes the latest action if multiple actions are dispatched.
  2. takeEvery: Takes every dispatched action.
  3. debounce: Delays execution until after wait time has elapsed since the last time it was invoked.
  4. 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!


This content originally appeared on DEV Community and was authored by Kawan Idrees


Print Share Comment Cite Upload Translate Updates
APA

Kawan Idrees | Sciencx (2024-07-28T11:12:54+00:00) Redux Saga for Efficient State Management in React. Retrieved from https://www.scien.cx/2024/07/28/redux-saga-for-efficient-state-management-in-react/

MLA
" » Redux Saga for Efficient State Management in React." Kawan Idrees | Sciencx - Sunday July 28, 2024, https://www.scien.cx/2024/07/28/redux-saga-for-efficient-state-management-in-react/
HARVARD
Kawan Idrees | Sciencx Sunday July 28, 2024 » Redux Saga for Efficient State Management in React., viewed ,<https://www.scien.cx/2024/07/28/redux-saga-for-efficient-state-management-in-react/>
VANCOUVER
Kawan Idrees | Sciencx - » Redux Saga for Efficient State Management in React. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/07/28/redux-saga-for-efficient-state-management-in-react/
CHICAGO
" » Redux Saga for Efficient State Management in React." Kawan Idrees | Sciencx - Accessed . https://www.scien.cx/2024/07/28/redux-saga-for-efficient-state-management-in-react/
IEEE
" » Redux Saga for Efficient State Management in React." Kawan Idrees | Sciencx [Online]. Available: https://www.scien.cx/2024/07/28/redux-saga-for-efficient-state-management-in-react/. [Accessed: ]
rf:citation
» Redux Saga for Efficient State Management in React | Kawan Idrees | Sciencx | https://www.scien.cx/2024/07/28/redux-saga-for-efficient-state-management-in-react/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.