Handling Middleware With Redux-Saga

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.

Complex State Management in React

Learn more about Redux in complex state management.

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

(

{

}

)

;

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

:

10

px

;

}

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.