Instagram
youtube
Facebook
Twitter

Middleware (Thunks, Sagas)

Introduction

Middleware in Redux is a powerful concept that allows you to extend the functionality of Redux by intercepting actions before they reach the reducer. It provides a way to handle asynchronous operations, logging, error handling, and more, making your Redux setup more robust and flexible. In this lesson, we will focus on two popular middleware for handling asynchronous actions in Redux: Redux Thunk and Redux Saga.

In this lesson, you’ll learn:

  • What middleware is in the context of Redux
  • How to set up and use Redux Thunk for asynchronous actions
  • How to set up and use Redux Saga for more complex asynchronous workflows

What is Middleware?

Middleware in Redux is a higher-order function that wraps the store’s dispatch method. It provides a third-party extension point between dispatching an action and the moment it reaches the reducer. This allows you to add custom logic, such as handling side effects or asynchronous operations.

Common use cases for middleware include:

  • Making API calls to fetch or send data.
  • Logging actions and state changes.
  • Implementing error handling.
  • Performing asynchronous actions in response to dispatched actions.

Setting Up Redux Thunk

Redux Thunk is a middleware that allows you to write action creators that return a function instead of an action. This function can perform asynchronous operations and dispatch actions when the operation is complete.

Step 1: Installing Redux Thunk

To use Redux Thunk, you need to install it alongside Redux:

npm install redux-thunk

Step 2: Applying Thunk Middleware

Next, you need to apply the thunk middleware when creating your Redux store.

// store.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import counterReducer from './reducer';

const store = createStore(counterReducer, applyMiddleware(thunk));

export default store;

In this example:

  • applyMiddleware(thunk) enhances the store to handle asynchronous action creators.

Creating Asynchronous Action Creators with Thunk

With Redux Thunk, you can create action creators that return a function. This function receives dispatch and getState as arguments, allowing you to perform asynchronous operations and dispatch actions based on the results.

Example: Fetching Data with Thunk

Suppose we want to fetch user data from an API.

Step 1: Define Action Types and Action Creators

// actions.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 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,
});

Step 2: Create the Asynchronous Action Creator

// actions.js

export const fetchUsers = () => {
  return (dispatch) => {
    dispatch(fetchUsersRequest());

    fetch('https://api.example.com/users')
      .then((response) => response.json())
      .then((data) => {
        dispatch(fetchUsersSuccess(data));
      })
      .catch((error) => {
        dispatch(fetchUsersFailure(error.message));
      });
  };
};

In this example:

  • fetchUsers is an asynchronous action creator that dispatches FETCH_USERS_REQUEST, fetches user data from an API, and then dispatches either FETCH_USERS_SUCCESS or FETCH_USERS_FAILURE based on the outcome.

Handling Async Actions in Reducers

You can handle the actions dispatched by your async action creator in your reducers.

// reducer.js

import {
  FETCH_USERS_REQUEST,
  FETCH_USERS_SUCCESS,
  FETCH_USERS_FAILURE,
} from './actions';

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

function userReducer(state = initialState, action) {
  switch (action.type) {
    case FETCH_USERS_REQUEST:
      return {
        ...state,
        loading: true,
      };
    case FETCH_USERS_SUCCESS:
      return {
        loading: false,
        users: action.payload,
        error: '',
      };
    case FETCH_USERS_FAILURE:
      return {
        loading: false,
        users: [],
        error: action.payload,
      };
    default:
      return state;
  }
}

export default userReducer;

In this example:

  • The reducer updates the state based on the different action types related to fetching users.

Setting Up Redux Saga

Redux Saga is a middleware that helps manage side effects in a Redux application more effectively. Unlike Redux Thunk, Redux Saga uses generator functions to handle complex asynchronous flows, making it easier to manage parallel requests, cancellations, and retries.

Step 1: Installing Redux Saga

To use Redux Saga, you need to install it alongside Redux:

npm install redux-saga

Step 2: Applying Saga Middleware

Just like with Redux Thunk, you need to apply the saga middleware when creating your Redux store.

// store.js

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducer';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));

sagaMiddleware.run(rootSaga);

export default store;

In this example:

  • createSagaMiddleware() creates the saga middleware, which is then applied to the Redux store.
  • sagaMiddleware.run(rootSaga) starts the root saga, allowing it to listen for dispatched actions.

Creating Sagas

A saga is a generator function that can yield effects, such as asynchronous actions or delays. You can create sagas to handle side effects.

Example: Fetching Users with Saga

Step 1: Create the Saga

// sagas.js

import { call, put, takeEvery } from 'redux-saga/effects';
import { FETCH_USERS_REQUEST, fetchUsersSuccess, fetchUsersFailure } from './actions';

function* fetchUsersSaga() {
  try {
    const response = yield call(fetch, 'https://api.example.com/users');
    const data = yield response.json();
    yield put(fetchUsersSuccess(data));
  } catch (error) {
    yield put(fetchUsersFailure(error.message));
  }
}

// Watcher saga
export function* watchFetchUsers() {
  yield takeEvery(FETCH_USERS_REQUEST, fetchUsersSaga);
}

// Root saga
export default function* rootSaga() {
  yield all([watchFetchUsers()]);
}

In this example:

  • fetchUsersSaga handles the side effect of fetching user data and dispatches success or failure actions.
  • watchFetchUsers listens for FETCH_USERS_REQUEST actions and triggers the fetchUsersSaga.

Using Sagas in the Application

To use the saga, dispatch the FETCH_USERS_REQUEST action from your component:

// actions.js

export const fetchUsers = () => ({
  type: FETCH_USERS_REQUEST,
});

Now you can use it in a component just like before:

import React from 'react';
import { useDispatch } from 'react-redux';
import { fetchUsers } from './actions';

const UserFetcher = () => {
  const dispatch = useDispatch();

  const handleFetchUsers = () => {
    dispatch(fetchUsers());
  };

  return (
    <button onClick={handleFetchUsers}>Fetch Users</button>
  );
};

export default UserFetcher;

Comparison: Redux Thunk vs. Redux Saga

Feature Redux Thunk Redux Saga
Syntax Simple function-based Uses generator functions
Complexity Easier for basic tasks More powerful for complex workflows
Error Handling Manual error handling Built-in effects for error handling
Side Effects Good for simple async operations Best for complex side effects
Testing Simpler to test More complicated but structured

Conclusion

In this lesson, you’ve learned:

  • The concept of middleware in Redux and its importance in managing side effects.
  • How to set up and use Redux Thunk for handling asynchronous actions.
  • How to set up and use Redux Saga for more complex asynchronous workflows.

Middleware like Redux Thunk and Redux Saga can significantly enhance your Redux application by managing side effects and asynchronous actions more effectively. In the next lesson, we will explore advanced Redux topics such as selectors, normalization, and integrating Redux with TypeScript.