Telerik blogs
ReactT2 Dark_1200x303

Managing a project’s state from the frontend may be stressful, especially if there is no specified logic. Redux-Saga makes things easier with the ability to test.

An essential task of a frontend developer is managing how data flows from the backend to the frontend. This includes managing the present state, sharing data between components and trying not to repeat the process of fetching the same data twice. Redux takes care of this task effortlessly.

In this article, we will be focusing more on state management with Redux and how to use Redux-Saga as a middleware to make state management a breeze.

Here is what we will be covering in the post:

  • Introduction to Redux
  • What is middleware?
  • Why middleware?
  • Introduction to Redux-Saga
  • How to set up Redux-Saga
  • How to use Redux-Saga With Redux
  • Saga Helper and Effect Creator
  • Using Saga in a React project

Prerequisite

To follow along with this post, you need to have:

  • Node installed on your PC
  • Basic understanding of React
  • Basic understanding of Redux
  • A text editor

Introduction to Redux

Redux is a central data store for all the data of an application. It helps any component from the application access the data it needs efficiently, making state management much easier to accomplish.

App.js has two branches: store and header. Under store are category and products. Under header are cart and menu. Store and cart both connect to a Redux central data store.

The image above contains a representation of a simple application flow. This flow is component-based. Let’s look at a scenario where the store component has all the data for the products to be used on the application. It will be easy if we want to pass the data to the category component or products component.

We can pass it as props, but it becomes tougher to achieve when we try to pass the data to the cart component. The path most developers take in resolving the issue is to move the data to the app component; then, the data will be passed as props down the components.

That helps, but it gets even more frustrating when dealing with a big project where you have a lot of components passing props. This approach may not be that effective, especially when you are looking from an optimization perspective—any changes to any component will trigger a refresh in all components with props related to it. That affects the users’ load time.

The way to effectively resolve this issue is to use a state management medium—Redux comes in here. As defined earlier, Redux is a central store where data are stored to be accessed by any component throughout the application.

What Is Middleware?

Middleware in Redux is a way to extend custom functionality; this gives extra features to the existing Redux. It provides third-party extension with points between the dispatching of action and the moment it reaches the reducer. Middleware can also be used for crash reporting, logging, asynchronous performance of a task, etc.

Why Middleware?

We use enhancers to override the dispatch function for Redux, but sometimes we are interested in customizing the dispatch function. Redux uses middleware for customizing the dispatch functions. Some other libraries like Express are using middleware too to customize specific behavior in an application.

Introduction to Redux-Saga

Redux-Saga is a companion library for Redux that effectively manages the asynchronous flow of an application. It allows the Redux store to communicate asynchronously with resources outside of the store—this includes accessing the local storage, HTTP requests and executing input and output services that are managed efficiently.

Redux-Saga is an example of a Redux middleware; other types include Redux Thunk, etc.

Getting Started

We will be creating a basic application that can fetch a list of users from an API, and we will be managing the state using Redux and Redux-Saga. Enter the command below into a terminal to create a React project.

npx create-react-app users

This command will create an empty create-react-app template. Open the project file on your preferred text editor.

Let’s install all the needed dependencies: react-redux, redux, redux-saga and bootstrap. Use the command below to install them.

yarn add react-redux redux redux-saga boostrap

Open the root folder and create a folder called redux. Inside, create two subfolders named actions and reducers. Lastly, create a file named store.js and add the following code inside the file.

import { createStore } from "redux";
import rootReducer from "./reducers";

const store = createStore(rootReducer);
export default store;

In the code above, we are importing createStore from Redux to create a Redux store, and we are importing the rootReducer, which contains all the reducers we will have in the project.

Next, we created a variable and assigned to it the store we will be creating. Now, let’s create our reducers. Firstly, inside the folder reducers, create an index.js file and a users.js file; the reducer we will be using in the project will be linked to the index.js file, while the file users.js will contain the user reducer. Paste the following code inside the index.js file:

import { combineReducers } from "redux";

const rootReducer = combineReducers({
 //All reducers will be here
});
export default rootReducer

We are using the combineReducers to combine all reducers into one place, which is the rootReducer. We will be adding the reducers inside later.

Now let’s work on our user reducer. Add the following code into the user.js file:

import * as types from '../types';

const initialState = {
 users: []
}

export default function users(state=initialState, action) {
 switch (action.type) {
  case type.GET_USERS;
   return {
    ...state,
    users: action.payload;
   }
  default: 
   return state;
 }
}

In the code above, we are importing types which we will be creating later, and then we are setting the initialState to the default state of the store; this is what we will be passing to the users’ reducer. Every reducer in Redux takes two parameters: the initial state and the action. The reducer makes use of a switch to check for the type of action that will be used to determine the return value.

We will now add the reducer to the rootReducer we created earlier. Let’s use this code below to update the reducers index.js file:

import { combineReducers } from "redux";
import Users from "./users";
const rootReducer = combineReducers({
  users: Users,
})
export default rootReducer;

Let’s create our types, create a types.js file inside the folder redux, and add the following code into the file:

export const GET_USERS = "GET_USERS";

Now, let’s create actions for our reducers. Create a users.js inside the actions folder and add the following code inside the file.

import * as types from "../types"

export function getUsers(users) {
 return {
  type: type.GET_USERS(),
  payload: users,
 }
}

Lastly, let’s add the provider to the index.js file in the root folder. Update the index.js file with the code below:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import store from './redux/store';
import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render(
 <Provider store={store}>
  <React.StrictMode>
   <App />
  </React.StrictMode>
 </Provider>,
 document.getElementById('root')
);

We are adding Provider as a wrapper to cover the entire project; this allows the data to be shared across our project. The Provider accepts the store we created containing the data we are storing.

Inside the card component, let’s add the following code.

import React from 'react'
const Card = ({user}) => {
  return (
    <div className="card">
      <div className="card-body">
        <div className="card-title">{user.name}</div>
        <div className="card-subtitle mb-2 text-muted">{user.company.name}</div>
        <div className="card-text">{user.company.catchPhrase}</div>
      </div>
    </div>
  )
}
export default Card

Inside the component, we get the user data as props and display it based on the user’s name, company and the company’s catchPhrase. Next, add the following code to the Users component.

import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getUser } from '../redux/actions/users'
import Card from "./Card"
const Users = () => {
  const dispatch = useDispatch()
  const users = useSelector(state => state.users.users)

  useEffect(() => {
    dispatch(getUser([{
     id: 1, 
     name: "Emmanuel",
     company: "Dusk",
     catchPhrase: "Made to fly"
    }]));
  }, [dispatch])
  return (
    <>
      {
        users.length > 0 && users.map(user => (
          <Card user={user} key={user.id} /> 
        ))
      }
      { users.length === 0 ? <p>No users</p> : null }
    </>
  )
}
export default Users

In the code above, we are importing useDispatch and useSelector. The useDispatch returns a dispatch reference from the store we created, while the useSelector allows us to extract data from the store.

We use the useSelector to get the users’ data from the store. In contrast, we use the useEffect method to set the users’ data using the dispatch function temporarily, pending the time we will be adding the middleware. We are iterating through the users’ data to get each user’s data passed to the card component.

Let’s update the app.css file with this style to give it the effect we want.

.App {
 margin: 5%;
}
.card {
 margin: 10px;
}

Now, let’s add redux dev so that we will be able to manage the state through it. Firstly open the store.js and update it with the code below.

import { createStore, compose } from 'redux';
import rootReducer from './reducers/index';
const store = compose(
  applyMiddleware(sagaMiddleware),
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  )(createStore)(rootReducer);
export default store;

Now, let’s set up our middleware. Create a subfolder in the src folder named saga and add index.js and userSaga.js files inside the folder.

Let’s start with the userSaga.js file—add the following code inside the file:

import { call, put, takeEvery } from 'redux-saga/effects';
const apiUrl = 'https://jsonplaceholder.typicode.com/users';
function getApiData() {
 return fetch(apiUrl).then(response => response.json().catch(error => error));
}

function* fetchUsers(action) {
 try {
  const users = yield call(getApiData);
  yield put({ type: 'GET_USERS_SUCCESS', users: users });
 } catch (error) {
  yield put({ type: 'GET_USERS_FAILED', message: error.message });
 }
}

function* userSaga() {
 yield takeEvery('GET_USERS_REQUESTED', fetchUsers);
}

export default userSaga;

Triggering a side effect from Redux-Saga is done through the process of yielding declarative effects. Redux-Saga will always compose these effects together to get a control flow working. The use of effects like call and put with takeEvery achieves the same aim as Redux Thunk, i.e., serves as a middleware with testability as an added benefit.

In the code above, we are importing put, call and takeEvery from Redux-Saga. We will be using these to get our middleware functionality. So we created an apiUrl variable to store the URL link for the API, and we also created a function getApiData that fetches the user data from the API endpoint.

Then we start creating a generator for the saga. The fetchUsers generator gets a parameter of actions, and it uses the try-catch method. The try method uses the call effect to yield the getApiData. Then, utilizing the put effect, it sets the type and action to the dispatch function based on the dispatch function.

Then we create the userSaga generator that takes the fetchUsers generator and uses the takeEvery effect to yield it to the GET_USER_REQUESTED type.

Lastly, let’s add this code to the index.js file in the subfolder saga.

import { all } from "redux-saga/effects";
import userSaga from "./userSaga";
export default function* rootSaga() {
 yield all([userSaga()]);
}

In the code above, we import all from redux-saga/effects and import the userSaga from the userSaga file we created earlier. We created a generator that yields the userSaga to the store using the effect all.

We will need to make some changes to our previous code. Open the store.js and update it with the code below.

import { createStore, compose, applyMiddleware } from 'redux';
import rootReducer from './reducers/index';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './saga/index';
const sagaMiddleware = createSagaMiddleware();
const store = compose(
  applyMiddleware(sagaMiddleware),
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  )(createStore)(rootReducer);
  sagaMiddleware.run(rootSaga);
export default store;

The changes above set the Redux-Saga we have been creating as middleware. Next, open your types.js file and update it with the code below.

export const GET_USERS_REQUESTED = 'GET_USERS_REQUESTED';
export const GET_USERS_SUCCESS = 'GET_USERS_SUCCESS';
export const GET_USERS_FAILED = 'GET_USERS_FAILED';

Now, open the reducers folder and update the users.js file with the following code.

import * as type from "../types";
const initalState = {
  users: [],
  loading: false,
  error: null
}
export default function users(state = initalState, action) {
  switch (action.type) {
    case type.GET_USERS_REQUESTED:
      return {
        ...state,
        loading: true
      }
    case type.GET_USERS_SUCCESS:
      return {
        ...state,
        loading: false,
        users: action.users
      }
    case type.GET_USERS_FAILED:
      return {
        ...state,
        loading: false,
        error: action.message
      }
    default:
      return state;
  }
}

In the code above, we updated the initial state and added the actions we created and the middleware to it. Go to the User component and update it with the following code.

import React, { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { getUser } from '../redux/actions/users'
import Card from "./Card"
const Users = () => {
  const dispatch = useDispatch()
  const users = useSelector(state => state.users.users)
  const loading = useSelector(state => state.users.loading)
  const error = useSelector(state => state.users.error)
  useEffect(() => {
    dispatch(getUser());
  }, [dispatch])
  return (
    <>
      {
        users.length > 0 && users.map(user => (
          <Card user={user} key={user.id} /> 
        ))
      }
      { users.length === 0 ? <p>No users</p> : null }
      { users.length === 0 && loading === true ? <p>Loading...</p> : null }
      { error === 0 && !loading === true ? <p>{error.message}</p> : null }
    </>
  )
}
export default Users

Lastly, add this update to the users.js file in the actions folder.

import * as types from "../types";
export function getUser(users) {
  return {
    type: types.GET_USERS_REQUESTED,
    payload: users,
  }
}

Now, everything is perfectly done. Open your terminal and run the project using the following command.

yarn start
//or

npm start

In your browser, you should see a page with content similar to that shown in the image below.

a list of cards with person's name, company, and slogan

Conclusion

In this post, we learned about Redux, middleware, why and where to use middleware, and Redux-Saga. We demonstrated all this using a simple project; you can easily replicate this for big projects to manage the state with ease.

Next up, you may want to learn about Recoil.

Chinedu
About the Author

Chinedu Imoh

Chinedu is a tech enthusiast focused on full-stack JavaScript and Infrastructure engineering.

Related Posts

Comments

Comments are disabled in preview mode.