How to Implement Redux-Saga With ReactJS and Redux [TUTORIAL]

Intro to Redux setup tutorial

Very often, you can hear about the state in frontend applications, but what it exactly is? Let me explain.

State in frontend applications represents all the data stored in the application in a given time. It can be stored in different formats like objects or strings. Based on the state’s values, we can personalize the application, display, and hide certain elements.

To manage the state in modern frontend frameworks, we can use different libraries like Redux, MobX, or NgRx. The most common one is Redux, which is used mainly with ReactJS applications, although it’s possible to use it with Angular as well.

With Redux, the state of the application is kept in the store, and we can access the store from every component in the application. Redux consist of store, reducers, and actions.

In this article, I’d like to tell you more about the Redux library and create a simple ReactJS project, where I’ll set up a Redux step by step.

Let’s start!

How does Redux work?

Redux is based on the flux architecture, and it supports unidirectional data flow. It means that data in the application goes through the same lifecycle over and over again, which makes everything that happens in the state more predictable.

Let’s take a look at the simple graphic, which illustrates the data’s lifecycle in the Redux application.

On the graphic above, you can see that from the UI, we trigger an action that passes the data to the reducer. Next, the reducer updates the store, which is the state of our application. The store defines the user interface.

Let’s think of the benefits which using Redux can bring to our development.

Benefits of using Redux

When you’re building the application, you know more or less how much data you will need to manage inside the application. In most cases, frontend applications have some functionality, and very rare they are just static websites. Commonly, we keep some user data, forms data, etc. inside the application state, and then it’s very useful to use a tool for managing the state.

The most popular solution in ReactJS applications is Redux. There are some important benefits to the popularity of this solution. Let’s take a look at them one by one.

  • predictable state - The state in Redux is predictable because reducer functions are pure; therefore, if we pass the same state and the same action, it needs to return the same result. Redux state is also immutable; it can’t be changed or modified.
  • easy to maintain - Considering that it’s predictable and very strict about the structure of the Redux application, anyone who knows Redux will understand it and work with it easily.
  • easy to debug - Redux allows us to log the behavior using available developer tools, makes debugging easier.
  • developer tools available - Redux has amazing developer tools, that can be used in the browser to see what’s happens in the backend.
  • server-side rendering - Redux supports server-side rendering by allowing to manage initial rendering. Redux sends the state of the application to the server with a response to the server’s request.

Above I listed a few benefits of using Redux to manage the state of your frontend application. Now, I’d like to go to the practical part, where we are going to set up a Redux with ReactJS application.

Create ReactJS project and install Redux

It’s time to start the practical part of this article. I have to create a new ReactJS application, which will be the base for our project. Then, I’ll install the Redux package, so I’ll be able to go to set it in our newly created application.

Open the console, and go to the location where you’d like to create the project. Use create-react-app. So, let’s create an application with the following command.

npx create-react-app redux-app

Next, let’s start the application using yarn or npm.

cd redux-app
yarn start

When your application works correctly, we have to install the redux package and react-redux package using the package manager you’ve selected for your project.

yarn add redux
yarn add react-redux

If everything is done, we can go to the code of our application and set up the Redux files structure.

Setup Redux structure

Right now, I have to set up the structure for our Redux files. I decided to create a separate folder for redux inside the src folder. There I created two folders, one for actions and one for reducers, and the last element I’ve created was store.js file.

└── src
    |── redux
    │   ├── actions
    │   ├── reducers
    │   |── store.js

When our Redux files’ structure is ready, we can connect the main file of our ReactJS application with the store.

Let’s open index.js file, and let’s update it as in the following code.

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import store from 'redux/store.js';

ReactDOM.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>,
  document.getElementById('root')
);

serviceWorker.unregister();

In the code above, I imported <Provider> component from react-redux, which is used to pass the Redux store to the ReactJS application.

The next thing we need to do at this point is to define our store. Let’s open store.js file, and let’s write the following code.

import { createStore } from 'redux';
import rootReducer from './reducers/index.js';

const store = createStore(rootReducer);

export default store;

As you could realize, inside the store.js file, I imported rootReducer, which needs to be created. In this file, I’m going to use combineReducers method, which will be used to combine reducers into a single reducer, that will be passed to the store. It’s used because to create and organize state, we mostly use more the one reducer, but we are able to pass just one reducer to the createStore method, that’s why we are using combineReducer.

Let’s open redux folder and create an index.js file there. Inside the newly created file, let’s use the following code.

import { combineReducers } from 'redux';
import users from './users';

const rootReducer = combineReducers({
  users: users,
});

export default rootReducer;

In the code above, I don’t pass any reducer yet, as I didn’t create any, but we will be updating this file. Now, let’s create an action and reducer.

Create action and reducer

In this step, I’m going to create a reducer and an action. In our application, we will use the JSONPlaceholder for getting data. We will create a list of user profiles. That’s why we are going to create a users reducer at first.

Let’s go to the reducers folder, and let’s create users.js file. Inside the file, let’s add the following code.

import * as type 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 this file, we set the users reducer, we also set the initial state and imported the type of action that will be used. No, we have to create the types.js file and create the type there. So, let’s go to the redux folder and create a file types.js and place inside the following code.

export const GET_USERS = 'GET_USERS';

Right now, we have to create an action to get users. Let’s go to the actions folder, and let’s create users.js file, where we are going to put actions.

Inside the file, we are going to define getUsers action with the following code.

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

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

In the code above, I created an action that will get users and save them in the reducers. Right now, we need some UI to dispatch the action and display data from our application store.

Dispatch action and get data from Redux store

Let’s start by creating a new component, where we will build UI for displaying data from the store. First of all, let’s add CDN that will allow us to use Bootstrap 5. Inside public\index.html file, add the following code in the head element.

<!-- CSS only -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css" integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I" crossorigin="anonymous">

Right now, let’s create a new folder for our components, and inside newly created components folder create two files, UsersComponent.jsx and CardComponent.jsx. Inside CardComponent.jsx file let’s add the following code.

import React from 'react';

const Card = (props) => {
  return (
    <div className="card">
      <div className="card-body">
        <h5 className="card-title">{props.user.name}</h5>
        <h6 className="card-subtitle mb-2 text-muted">{props.user.company.name}</h6>
        <p className="card-text">{props.user.company.catchPhrase}</p>
      </div>
    </div>
  )
}

This code is used to create a user card with the user name, company name, and company phrase.

Next, let’s open UsersComponent.jsx file, and let’s put there the following code.

import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { getUsers } from '../redux/actions/users';
import Card from './CardComponent';

const Users = () => {
  const dispatch = useDispatch();
  const users = useSelector(state => state.users.users);

  useEffect(() => {
    dispatch(getUsers([
      {
        id: 1,
        name: 'Leanne Graham',
        company: {
          name: "Romaguera-Crona",
          catchPhrase: "Multi-layered client-server neural-net",
        }
      }
    ]));
  }, [])

  return (
    <>
      {users.length > 0 && users.map((user) => (
        <Card key={user.id} user={user} />
      ))}
      {users.length === 0 && <p>No users available!</p>}
    </>
  )
}

export default Users;

We used hooks from react-redux and useEffect() where I’m dispatching the action in this code. When we run the application, the action is dispatched, and user data is passed to the store.

We still need to add our UsersComponent to App.js file to display it and change some styles. Let’s open App.js first and make sure it looks like the following code.

import React from 'react';
import Users from './components/UsersComponent';
import './App.css';

function App() {
  return (
    <div className="App">
      <Users />
    </div>
  );
}

export default App;

And let’s open App.css file right now; next, delete all the styles inside it and place the code like below.

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

To be able to check what’s happening in Redux, we can use redux-dev-tools, which we are going to turn on in the next point.

Add redux-dev-tools

redux-dev-tools is a tool that allows us to check what happens in our application state, which actions are dispatched, and what data is in the store.

Let’s open our store.js file, and let’s update it with the following code.

import { createStore, compose } from 'redux';

const store = compose(
  window.devToolsExtension && window.devToolsExtension(),
)(createStore)(rootReducer);

Right now, when you will open developer tools in Google Chrome and find the Redux tab, you will be able to see all the information about the store and actions happening in Redux.

Setup Redux middleware

At first, let me explain what middleware is. A code can be placed between the frameworks that send a request and the frameworks that generate the response. The big advantage of middleware is that we can combine a few third-party middlewares in one project.

So, why do we need middleware in Redux? The data flow between action and reducer works according to a pretty clear pattern, but when we have to communicate with the API or do some other side effect type of action. Middleware helps to perform side effects without blocking the app’s state updates.

In this article, I’d like to go deeper in the situation when we have to communicate with API through the Redux. That’s why I’d like to tell you more about two popular middleware solutions for asynchronous API calls with Redux, Redux-Thunk, and Redux-Saga.

Redux Thunks

Redux Thunks is a third party library, allowing to create an asynchronous API call inside the Redux application. It allows us to write the function, called a thunk, which makes the Ajax request and calls the action creator with the response’s data.

Now, let me explain what a thunk is. Thunk is a wrapper function that delays the expression evaluation.

Redux Thunks are very common among beginners in ReactJS and Redux environment, as it’s pretty easy to use and set up.

But, we will not select this solution for our API call.

There is a different popular solution for the middleware in Redux, and it’s called Redux-Saga. Let’s take a closer look at this topic right now.

Redux Saga

The next solution for middleware is Redux-Saga. Redux-Saga uses ES6 Generators instead of functions. It allows us to easily test, write, and read the asynchronous calls in Redux.

The big advantage of using Redux-Saga instead of Redux-Thunk is avoiding callback hell, and the actions stay pure, so the asynchronous code is pretty easy to test. In our application, we are going to use Redux-Saga as a middleware to create API calls. Let’s implement it!

Implementing middleware

To implement our redux-saga let’s start by installing it using yarn or npm.

yarn add redux-saga

Right now, let’s create saga folder inside the redux folder. Inside the newly created folder, create two files, index.js and userSaga.js. Inside userSaga.js file, we will create an API call and our sagas to fetch user data.

import { call, put, takeEvery } from 'redux-saga/effects'

const apiUrl = `https://jsonplaceholder.typicode.com/users`;
function getApi() {
  return fetch(apiUrl, {
      method: 'GET',
      headers: {
          'Content-Type': 'application/json',

      }
  }).then(response => response.json())
    .catch((error) => {throw error})
}

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

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

export default userSaga;

Great, when that’s ready, let’s open the index.js file, and we have to create an object that will combine our sagas, because we may have more than one.

import { all } from 'redux-saga/effects'
import userSaga from './userSaga'

export default function* rootSaga() {
  yield all([
    userSaga(),
  ])
}

The next step is to apply middleware and run our rootSaga inside store.js file.

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers/index.js';
import rootSaga from './sagas/index';

const sagaMiddleware = createSagaMiddleware();
const store = compose(
  applyMiddleware(sagaMiddleware),
  window.devToolsExtension && window.devToolsExtension(),
)(createStore)(rootReducer);

sagaMiddleware.run(rootSaga);

export default store;

Great, right now, we can do changes in our reducer to listen for the result of the action, but first, let’s change and add new types in our types.js file.

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

Open the reducer\user.js file, and let’s update the reducer and initial state.

const initialState = {
  users: [],
  loading: false,
  error: null,
}

export default function users(state = initialState, 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
  }
}

Right now, we have error and loading properties in our reducer. When the API call starts, we can turn on the loader to let the user know what’s going on.

Let’s go to the user.js file in the actions folder, to change the type of the actions.

export function getUsers() {
  return {
    type: type.GET_USERS_REQUESTED,
  }
}

Because of those changes we need to apply some changes in our UsersComponent.jsx file.

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

  return (
    <>
      {users.loading && <p>Loading...</p>}
      {users.length === 0 && !loading && <p>No users available!</p>}
      {error && !loading && <p>{error}</p>}
      {users.length > 0 && users.map((user) => (
        <Card key={user.id} user={user} />
      ))}
    </>
  )
}

Great, let’s see if the application works correctly!

Result

When you open the application and the developer tools, you will see that the request action is first started, then reducer change loading to be true. When the call is ready, the success action should happen, and the data should be displayed on the screen.

Here’s it looks for me.

Conclusion

Congratulations! You’ve just created a ReactJS application with Redux and with sagas middleware.

In this article, you could learn what’s application state, why state management library is a good solution in some cases, and how to set up Redux in ReactJS application. Besides that, you could also find out what’s middleware and why we need to use in with ReactJS and Redux. We also compared redux-thunks and redux-saga.

Let us know which solution do you prefer in your apps.

Thank you for reading,
Anna