DEV Community

Cover image for Mastering Redux-Saga: Advanced Concepts and Use Cases 🌪️
Tejas Nikhar
Tejas Nikhar

Posted on

1 2 1

Mastering Redux-Saga: Advanced Concepts and Use Cases 🌪️

While implementing the feature for creating project versions at my organization, I leveraged Redux-Saga's channel feature to schedule a recurring version creation action every 15 minutes. The way Redux-Saga manages channels and encapsulates complex logic for developers fascinated me. Despite its powerful capabilities, Redux-Saga is surprisingly underrated in the React community.

This article explores key Redux-Saga concepts and why they can be invaluable for React developers using middleware.


Introduction

Redux-Saga is a powerful middleware for handling side effects in Redux applications. While many developers are familiar with basic effects like takeLatest and call, the library offers advanced utilities that make it an underrated powerhouse for managing complex async flows.

In this article, we'll cover:

  • Essential Redux-Saga effects: put, call, fork, all, take, takeLatest, takeEvery,delay
  • Managing concurrent tasks with race
  • Leveraging actionChannel for queued processing
  • Using Redux-Saga channels for more controlled event handling
  • Practical examples and best practices

Understanding Key Redux-Saga Effects

put

Dispatches an action to the Redux store.

import { put } from "redux-saga/effects";

yield put({ type: "FETCH_SUCCESS", payload: data });
Enter fullscreen mode Exit fullscreen mode

call

Calls a function (usually an API request) and waits for its response.

import { call } from "redux-saga/effects";

yield call(api.fetchUser, userId);
Enter fullscreen mode Exit fullscreen mode

fork

Spawns a non-blocking task.

import { fork } from "redux-saga/effects";

yield fork(watchUserLogin);
Enter fullscreen mode Exit fullscreen mode

all

Runs multiple sagas in parallel.

import { all, call } from "redux-saga/effects";

yield all([call(fetchUsers), call(fetchPosts)]);
Enter fullscreen mode Exit fullscreen mode

take

Waits for a specific action before proceeding.

import { take } from "redux-saga/effects";

yield take("LOGIN_REQUEST");
Enter fullscreen mode Exit fullscreen mode

takeLatest

Only the latest invocation of the saga will be executed, canceling previous ones.

import { takeLatest } from "redux-saga/effects";

yield takeLatest("FETCH_USER", fetchUserSaga);
Enter fullscreen mode Exit fullscreen mode

takeEvery

Runs a saga for every action occurrence without canceling previous executions.

import { takeEvery } from "redux-saga/effects";

yield takeEvery("ADD_ITEM", addItemSaga);
Enter fullscreen mode Exit fullscreen mode

delay

Delays execution for a specified time.

import { delay } from "redux-saga/effects";

yield delay(2000); // 2 seconds delay
Enter fullscreen mode Exit fullscreen mode

Sign-Up Example: Combining Multiple Redux-Saga Effects

import { Action } from "@reduxjs/toolkit";
import {
  CallEffect,
  PutEffect,
  call,
  put,
  takeLatest,
} from "redux-saga/effects";
import { AxiosError, AxiosResponse } from "axios";
import { signUpRequest, signUpSuccess, signUpError } from "../actions";
import { SignUpRequest, SignUpSuccess, SignUpError } from "../../types/store";
import { signUpUser } from "../../api/signUp";
import { handleErrors } from "../../utils/error.handler";

function* signUp(
  action: Action & { payload: SignUpRequest; type: string }
): Generator<
  | CallEffect<AxiosResponse<void> | AxiosError<unknown>>
  | PutEffect<{ payload: SignUpSuccess | SignUpError; type: string }>
  | unknown,
  void,
  AxiosResponse<void>
> {
  try {
    if (signUpRequest.match(action)) {
      const response: AxiosResponse<void> = yield call(
        signUpUser,
        action.payload
      );
      if (response.status !== 201) {
        throw new Error("SignUp failed!");
      }
      yield put(
        signUpSuccess({
          error: null,
          success: true,
          feedbackMessage: "User Sign Up Successful",
        })
      );
    }
  } catch (error) {
    yield* handleErrors(error, signUpError);
  }
}

function* watchSignUpRequest(): Generator {
  yield takeLatest(signUpRequest.type, signUp);
}

export { watchSignUpRequest };
Enter fullscreen mode Exit fullscreen mode

Leveraging Redux-Saga Channels for Auto Version Creation

Initial Implementation Using take and delay

The initial implementation used a simple while (true) loop with delay. Every AUTOSAVE_TIMEOUT milliseconds, a new autosave request was dispatched.

const autoCreateCanvasVersion = function* (params: CreateVersionRequest) {
  while (true) {
    yield put(createCanvasVersionRequest(params));
    yield delay(AUTOSAVE_TIMEOUT);
  }
};
Enter fullscreen mode Exit fullscreen mode

While this approach works, it has several drawbacks:

  • Lack of Event Control: The loop runs indefinitely, making it difficult to stop the autosave process dynamically.
  • Inefficient Resource Usage: It keeps running even if the autosave feature is disabled, leading to unnecessary function executions.

To address these issues, we introduce eventChannel in the improved implementation.

Improved Implementation Using Channels

In the improved version, we use eventChannel to emit events at an interval, allowing external control over when autosave occurs. Instead of manually handling delays, Redux-Saga now reacts to incoming events efficiently.

import { eventChannel, buffers } from "redux-saga";
import {
  take,
  put,
  call,
  fork,
  select,
  takeEvery,
  cancel,
} from "redux-saga/effects";

// Creates an event channel that emits an autosave trigger at a fixed interval
const createAutoSaveChannel = () =>
  eventChannel((emit) => {
    const interval = setInterval(() => {
      emit(true);
    }, AUTOSAVE_TIMEOUT);

    return () => clearInterval(interval);
  }, buffers.sliding(1));

function* autoCreateCanvasVersion(params: CreateVersionRequest) {
  // Check version history flag once at the start
  const versionHistoryEnabled = yield select(selectVersionHistoryEnabled);
  if (!versionHistoryEnabled) return;

  const channel = yield call(createAutoSaveChannel);

  try {
    while (true) {
      yield take(channel);
      yield put(createCanvasVersionRequest(params));
    }
  } finally {
    channel.close();
  }
}

function* watchAutoCreateCanvasVersion() {
  yield takeEvery(startAutoCreate.type, function* (action) {
    const params: CreateVersionRequest = action.payload;
    const task = yield fork(autoCreateCanvasVersion, params);

    yield take(stopAutoCreate.type);
    yield cancel(task);
  });
}
Enter fullscreen mode Exit fullscreen mode

Improvements Using Channels

  1. Event-Driven Autosaving:

    Instead of an infinite loop using delay, we now use an eventChannel that emits at fixed intervals. This prevents blocking issues and ensures that autosave events are managed externally.

  2. Sliding Buffer Optimization:

    We use a sliding(1) buffer so that if multiple autosave events are triggered while a previous one is still running, only the latest one is kept, avoiding unnecessary queuing of outdated save requests.

  3. Efficient Stopping Mechanism:

    • We check versionHistoryEnabled only once at saga startup and avoid unnecessary Redux state checks.
    • Instead of relying on Redux state updates, we stop autosaving when stopAutoCreate is dispatched by cancelling the task.
    • The finally block ensures that the event channel is closed when the saga exits, preventing memory leaks.

Conclusion

Redux-Saga offers an incredibly powerful way to manage side effects in Redux applications. With features like actionChannel and eventChannel, developers can gain fine-grained control over asynchronous processes.

If you’re interested in exploring more, check out the official Redux-Saga documentation: Redux-Saga Homepage.

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs