Skip to content
APPCITORThe Tech Blog
Esc

Choose the Right Redux Approach: Saga, Thunk, or Toolkit?

June 11, 2023 8 min read

redux-saga-thunk-toolkit
On this page

Hey there! Today, we’re going to dive into two popular middleware options frequently utilized in JavaScript projects. One of them is Redux Saga, which I’ve previously covered in detail in a dedicated article. If you’re interested in understanding how middleware works in React Redux, I highly recommend checking it out. In that article, I provide a comprehensive guide on setting up Redux Saga along with a live example to help you grasp the concepts better. So, let’s explore these middleware options and enhance our understanding of handling asynchronous operations in JavaScript projects!

Redux saga setup and use

Before we begin the explanation, let me provide you with a quick overview of the project.
For this example, I’m using the React project and
Our primary focus revolves around calling two API endpoints.
In this article, I’m not explaining other relevant things; I’m only explaining the middleware part.

const GET_POSTS_URL = "https://jsonplaceholder.typicode.com/users/1/posts";
const POST_POSTS_URL = "https://jsonplaceholder.typicode.com/posts";

One for get the posts for a specific user and the other is for creating a post under a specific user in this case the user id will 1.

Redux Thunk

import axios from "axios";
import { GET_POSTS_URL, POST_POSTS_URL } from "../constant";

import {
  createPostRequest,
  createPostError,
  createPostSuccess,
  getPostsRequest,
  getPostsError,
  getPostsSuccess
} from "./action-creators";

const fetchPosts = () => {
  return async (dispatch) => {
    dispatch(getPostsRequest());
    try {
      const response = await axios.get(GET_POSTS_URL);
      dispatch(getPostsSuccess(response.data));
    } catch (error) {
      dispatch(getPostsError(error.message));
    }
  };
};

const createPost = (postData) => {
  return async (dispatch) => {
    dispatch(createPostRequest());
    try {
      const response = await axios.post(POST_POSTS_URL, postData);
      dispatch(createPostSuccess(response.data));
    } catch (error) {
      dispatch(createPostError(error.message));
    }
  };
};

export { fetchPosts, createPost };

The given code is a module that enables fetching and creating posts through HTTP requests. It utilizes the axios library, a popular JavaScript library for handling HTTP requests. Additionally, it imports the necessary URLs and action creator functions from other files.

To fetch posts, the module provides the fetchPosts function. When this function is called, it initiates an asynchronous operation. Within this operation, it notifies the application that the request to fetch posts has started by dispatching the getPostsRequest action. Then, it employs axios to send a GET request to the specified GET_POSTS_URL endpoint. Upon receiving a response, it dispatches the getPostsSuccess action with the retrieved data. In case an error occurs during the request, it dispatches the getPostsError action with the error message.

For creating posts, the module offers the createPost function. Similar to fetchPosts, it is an asynchronous function that initiates an operation. It dispatches the createPostRequest action to indicate the initiation of the request to create a new post. Using axios, it sends a POST request to the designated POST_POSTS_URL endpoint, including the provided postData as the request payload. Upon a successful request, it dispatches the createPostSuccess action with the response data. If an error occurs, it dispatches the createPostError action with the associated error message.

To make use of these functionalities, you can import and call the fetchPosts and createPost functions within your code. They provide an interface for interacting with the server, allowing you to retrieve existing posts or create new ones seamlessly.


Redux Saga

import axios from "axios";
import { call, put, takeLatest } from "redux-saga/effects";
import { GET_POSTS_URL, POST_POSTS_URL } from "../constant";
import {
  GET_POSTS,
  GET_POSTS_ERROR,
  GET_POSTS_SUCCESS,
  POST_POSTS,
  POST_POSTS_ERROR,
  POST_POSTS_SUCCESS
} from "../types/post-type";

function* fetchPosts() {
  try {
    const response = yield call(axios.get, GET_POSTS_URL);
    yield put({ type: GET_POSTS_SUCCESS, payload: response.data });
  } catch (e) {
    yield put({ type: GET_POSTS_ERROR, payload: e.message });
  }
}

function* createPosts(action) {
  try {
    const response = yield call(axios.post, POST_POSTS_URL, action.payload);
    yield put({ type: POST_POSTS_SUCCESS, payload: response.data });
  } catch (e) {
    yield put({ type: POST_POSTS_ERROR, payload: e.message });
  }
}

function* postSaga() {
  yield takeLatest(GET_POSTS, fetchPosts);
  yield takeLatest(POST_POSTS, createPosts);
}

export default postSaga;

The provided code is a Redux Saga module that handles asynchronous operations related to fetching and creating posts. It utilizes the axios library for making HTTP requests and the redux-saga library for managing asynchronous actions in Redux.

After importing the necessary dependencies and constants, the module defines three generator functions: fetchPosts, createPosts, and postSaga.

The fetchPosts generator function is responsible for fetching posts. It is triggered when a GET_POSTS action is dispatched. Inside the function, it uses the call effect from redux-saga to call the axios.get function with the GET_POSTS_URL endpoint. The response from the API call is stored in the response variable using the yield keyword. Then, it dispatches a Redux action of type GET_POSTS_SUCCESS with the data from the response. If an error occurs during the API call, it dispatches a Redux action of type GET_POSTS_ERROR with the error message.

The createPosts generator function handles the creation of posts. It is triggered when a POST_POSTS action is dispatched. The action parameter contains the payload data necessary for creating the post. Inside the function, it uses the call effect to call the axios.post function with the POST_POSTS_URL endpoint and the payload data from the action. The response from the API call is stored in the response variable using the yield keyword. Then, it dispatches a Redux action of type POST_POSTS_SUCCESS with the data from the response. If an error occurs during the API call, it dispatches a Redux action of type POST_POSTS_ERROR with the error message.

The postSaga generator function is the main entry point for the saga. It uses the takeLatest effect from redux-saga to listen for GET_POSTS and POST_POSTS actions. When a GET_POSTS action is detected, it calls the fetchPosts generator function. When a POST_POSTS action is detected, it calls the createPosts generator function.

Finally, the postSaga generator function is exported as the default export of the module, allowing it to be used in the Redux store configuration to handle asynchronous actions related to posts.


Redux Toolkit

import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { GET_POSTS_URL, POST_POSTS_URL } from "../constant";

export const fetchPosts = createAsyncThunk("get/posts", async () => {
  const response = await axios.get(GET_POSTS_URL);
  return response.data;
});

export const createPosts = createAsyncThunk("post/posts", async (postData) => {
  const response = await axios.post(POST_POSTS_URL, postData);
  return response.data;
});

export const PostsSlice = createSlice({
  name: "posts",
  initialState: {
    loading: false,
    posts: [],
    error: null
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchPosts.pending, (state) => {
        state.loading = true;
      })
      .addCase(fetchPosts.fulfilled, (state, action) => {
        state.loading = false;
        state.posts = action.payload;
      })
      .addCase(fetchPosts.rejected, (state, action) => {
        state.loading = false;
        state.posts = [];
        state.error = action.error.message;
      })
      .addCase(createPosts.pending, (state) => {
        state.loading = true;
      })
      .addCase(createPosts.fulfilled, (state, action) => {
        state.loading = false;
        state.posts.push(action.payload);
      })
      .addCase(createPosts.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message;
      });
  }
});

export default PostsSlice.reducer;

The provided code is a part of a Redux Toolkit implementation that handles fetching and creating posts. It leverages the axios library for HTTP requests and imports necessary functions from Redux Toolkit.

The code begins by importing the required functions and libraries, including the axios library and URLs for fetching and creating posts.

Two main functions, namely fetchPosts and createPosts, are defined as thunks to handle asynchronous operations.

The fetchPosts thunk is responsible for fetching posts by sending a GET request to the specified URL using axios. It then returns the response data.

The createPosts thunk is responsible for creating new posts. It does so by sending a POST request to the specified URL, along with the provided post data. The response data is then returned.

The code proceeds to define a PostsSlice using the createSlice function from Redux Toolkit. This slice sets the initial state with properties to track the loading status, posts data, and any encountered errors.

The extraReducers section within the PostsSlice definition handles the actions dispatched by the thunks. It updates the state based on different actions such as pending, fulfilled, and rejected.

Regarding the fetchPosts thunk, the extraReducers specifies how the state should be modified for each action. When the request is pending, the loading status is set to true. When the request is fulfilled, the loading status is set to false, and the received posts data is stored. In case of an error, the loading status is set to false, the posts data is cleared, and the error message is stored.

Similarly, for the createPosts thunk, the extraReducers handles the loading status and error message based on the request’s status.

Finally, the PostsSlice.reducer is exported as the default export, allowing it to be utilized for configuring the Redux store and managing the state related to posts in an application.

Pros and cons

Redux ThunkRedux SagaRedux Toolkit
Async Control FlowBasic control flow for async actions.Advanced control flow capabilities, including pausing, canceling, and retrying actions.Basic control flow for async actions.
ComplexitySuitable for simpler async scenarios with straightforward logic.Suitable for complex scenarios involving intricate control flow, error handling, and coordination of multiple async operations.Suitable for simpler async scenarios with straightforward logic.
Learning curveEasy to learn and understand for developers familiar with Redux.Requires understanding of generator functions and Saga effects.Easy to learn and understand for developers familiar with Redux.
Boilerplate CodeRequires less code compared to Redux Saga.Requires more code for setting up and defining Sagas.Requires less code compared to Redux Saga.
Recommended Use CasesSimpler async scenarios with straightforward logic.Complex async scenarios, real-time data updates, long-running processes.Simpler async scenarios with straightforward logic.

Let me briefly explain the Async Control Flow of the above section
Async control flow refers to the management and coordination of asynchronous actions in a program. It involves controlling the order, timing, and flow of asynchronous operations such as API calls, database queries, or file I/O. Async control flow libraries like Redux Saga provide advanced capabilities such as pausing, canceling, retrying, and handling complex scenarios involving multiple asynchronous tasks. These libraries offer a way to handle async actions in a more organized and efficient manner, allowing developers to control the execution and flow of async operations to meet specific requirements.

Example

function* createPosts(action) {
  try {
    // Make API call to create post 
    const post = yield call(createPostUrl, action.payload);

    // Dispatch success action with post data
    yield put(createPostsSuccess(post));
  } catch (error) {
    // Dispatch failure action if API call fails
    yield put(createPostFailure(error.message));

    // Retry or cancel action based on user input with a delay
    while (true) {
      yield delay(2000); // Delay for 2 seconds before retrying

      const retryAction = yield take(retryCreatePost);
      if (retryAction.payload.postId === action.payload.postId) {
        break; // Retry the action
      }
    }
  }
}

Live Example

I made all these sections separate modules. You can uncomment the index.js commented section as you want. When considering each module, you will see all reducer, action, action-creators, and store files are there.  It will be easy to understand the module.

// state handle using redux toolkit
// import App from "./toolkit/App";

// state handle using redux saga
// import App from "./saga/App";

// state handle using redux thunk
// import App from "./thunk/App";

Thank you guys, let’s meet on another article.

Share this article

Keep reading