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
Event-Driven Autosaving:
Instead of an infinite loop usingdelay
, we now use aneventChannel
that emits at fixed intervals. This prevents blocking issues and ensures that autosave events are managed externally.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.-
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.
- We check
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.
Top comments (0)