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 dispatchesFETCH_USERS_REQUEST
, fetches user data from an API, and then dispatches eitherFETCH_USERS_SUCCESS
orFETCH_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 forFETCH_USERS_REQUEST
actions and triggers thefetchUsersSaga
.
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.