Redux Saga for Efficient State Management in React

kawanedres

Kawan Idrees

Posted on July 28, 2024

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

Enter fullscreen mode Exit fullscreen mode

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" }
  ]
}

Enter fullscreen mode Exit fullscreen mode

Start the JSON Server:

json-server --watch db.json --port 5000

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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
});


Enter fullscreen mode Exit fullscreen mode
// 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;

Enter fullscreen mode Exit fullscreen mode

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';

Enter fullscreen mode Exit fullscreen mode
// 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 });

Enter fullscreen mode Exit fullscreen mode

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)
  ]);
}

Enter fullscreen mode Exit fullscreen mode

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()
  ]);
}

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;

Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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!

💖 💪 🙅 🚩
kawanedres
Kawan Idrees

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