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 }); call Calls a function (usually an API request) and waits for its response. import { call } from "redux-saga/effects"; yield call(api.fetchUser, userId); fork Spawns a non-blocking task. import { fork } from "redux-saga/effects"; yield fork(watchUserLogin); all Runs multiple sagas in parallel. import { all, call } from "redux-saga/effects"; yield all([call(fetchUsers), call(fetchPosts)]); take Waits for a specific action before proceeding. import { take } from "redux-saga/effects"; yield take("LOGIN_REQUEST"); 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); takeEvery Runs a saga for every action occurrence without canceling previous executions. import { takeEvery } from "redux-saga/effects"; yield takeEvery("ADD_ITEM", addItemSaga); delay Delays execution for a specified time. import { delay } from "redux-saga/effects"; yield delay(2000); // 2 seconds delay 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 { try { if (signUpRequest.match(action)) { const response: AxiosResponse = 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 }; 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); } }; 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 tha

Feb 13, 2025 - 07:17
 0
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 });

call

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

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

yield call(api.fetchUser, userId);

fork

Spawns a non-blocking task.

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

yield fork(watchUserLogin);

all

Runs multiple sagas in parallel.

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

yield all([call(fetchUsers), call(fetchPosts)]);

take

Waits for a specific action before proceeding.

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

yield take("LOGIN_REQUEST");

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);

takeEvery

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

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

yield takeEvery("ADD_ITEM", addItemSaga);

delay

Delays execution for a specified time.

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

yield delay(2000); // 2 seconds delay

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 };

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);
  }
};

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);
  });
}

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.