Home
React
Redux Toolkit vs Redux Saga
Daniel Nguyen
Daniel Nguyen
August 07, 2025
2 min

Redux Toolkit answers: What is the current application state?

Redux Saga answers: What should happen next when an action occurs?

Redux Toolkit simplifies state management, while Redux Saga orchestrates complex asynchronous workflows such as task queues, cancellation, timeouts, and concurrent execution.

Let’s explore these concepts through a realistic example: a Concurrent Test Runner.

Redux Toolkit

1. The Problem: Managing Concurrent Tasks

Imagine an application that runs automated tests. Multiple users can submit test runs, but the system allows only three tests to execute simultaneously.

Running:
#101
#102
#103
Queue:
#104
#105

When test #101 finishes, the scheduler starts #104.

What if a user cancels #103? Or test #102 exceeds its timeout?

We need a system that can coordinate workers, enforce concurrency limits, handle failures, and keep Redux state synchronized.

That’s where Redux Saga becomes useful.

2. Reading and Updating Redux State

Redux Saga communicates with Redux through effects such as select and put.

select reads the current Redux state:

const queue = yield select(selectQueue);
const runningIds = yield select(selectRunningIds);
const maxConcurrency = yield select(selectMaxConcurrency);

put dispatches an action:

yield put(runStarted(runId));
yield put(runCompleted(runId));

Use select to make scheduling decisions and put to report state changes.

3. Running Tasks Concurrently

Redux Saga provides several effects for managing asynchronous work.

EffectPurpose
callExecute a function and wait for its result
forkStart a Saga task without blocking the caller
joinWait for a task to finish
cancelCancel a running Saga task
raceRun competing effects and keep the first result

For example:

const task = yield fork(runTest, runId);

The returned task represents the running Saga. The scheduler can track it and cancel it later.

To wait for multiple workers:

const tasks = [];
for (const user of users) {
tasks.push(yield fork(runUser, user));
}
for (const task of tasks) {
yield join(task);
}

Each worker starts independently, while join lets the parent wait for completion.

4. Cancellation and Cleanup

Dispatching a Redux action that marks a test as cancelled does not, by itself, stop its Saga task.

Use cancel(task) to request cancellation:

function* runTest(runId: string) {
try {
yield call(executeTests, runId);
yield put(runCompleted(runId));
} finally {
if (yield cancelled()) {
yield put(runCancelled(runId));
}
}
}

When the Saga is cancelled, its finally block executes, allowing cleanup and state synchronization.

Important: Cancelling a Saga does not automatically terminate arbitrary external work. The underlying operation must support cancellation or cleanup if it needs to stop too.

5. Handling Timeouts with race

Suppose a test must finish within 30 seconds.

const result = yield race({
test: call(executeTests, runId),
timeout: delay(30_000),
});

The first effect to finish determines the result.

  • result.test contains the test result if execution finishes first.
  • result.timeout is set if the delay finishes first.

The losing Saga effect is cancelled. However, the underlying test process may need an explicit cancellation mechanism.

You can also race against a cancellation action:

const result = yield race({
test: call(executeTests, runId),
timeout: delay(30_000),
cancel: take(action => cancelRun.match(action)
&& action.payload === runId),
});

Now the worker can react to test completion, timeout, or user cancellation. The caller must inspect the winning key and update the run status accordingly.

6. Choosing takeEvery vs. takeLatest

These helpers determine how Saga responds to repeated actions.

takeEvery

yield takeEvery(runRequested.type, handleRunRequested);

Starts a new worker for every matching action. This is useful when each submitted test run must be handled independently.

However, takeEvery does not enforce a concurrency limit by itself. The scheduler must manage the queue and active worker count.

takeLatest

yield takeLatest(searchRequested.type, searchSaga);

Cancels the previous Saga task when a newer matching action arrives.

For example:

search("rea") → cancelled
search("reac") → cancelled
search("react") → latest task continues

This works well for search requests, where older results are no longer needed.

7. Where Redux Toolkit Fits In

Redux Toolkit remains the recommended standard approach for Redux state management.

  • createSlice defines reducers and generates action creators.
  • configureStore configures the store and common middleware.
  • createAsyncThunk handles straightforward asynchronous operations.
  • RTK Query manages server-data fetching and caching.
  • Redux Saga coordinates complex workflows involving concurrency, cancellation, and long-running tasks.

For example, a slice can store the test runner’s state:

type TestRun = {
id: string;
status: "queued" | "running" | "completed" | "failed" | "cancelled" | "timeout";
};
const testRunsSlice = createSlice({
name: "testRuns",
initialState: { runs: [] as TestRun[] },
reducers: {
runStarted(state, action: PayloadAction<string>) {
const run = state.runs.find(
item => item.id === action.payload
);
if (run) run.status = "running";
},
runCompleted(state, action: PayloadAction<string>) {
const run = state.runs.find(
item => item.id === action.payload
);
if (run) run.status = "completed";
},
},
});

Saga coordinates execution and dispatches actions; Redux Toolkit reducers update the state that the UI reads.

Summary

Redux Toolkit and Redux Saga solve complementary problems:

  • Redux Toolkit manages predictable application state.
  • Redux Saga orchestrates complex asynchronous workflows.
  • fork and join coordinate background tasks.
  • cancel and finally support cancellation and cleanup.
  • race handles competing outcomes such as completion, timeout, and cancellation.
  • takeEvery and takeLatest control how repeated actions spawn or cancel workers.

Use Redux Saga when workflow complexity justifies it. For simpler asynchronous requests, createAsyncThunk or RTK Query is often a better fit.


Tags

#redux

Share

Daniel Nguyen

Daniel Nguyen

Frontend Developer

Frontend developer specializing in React, Next.js, and JavaScript. Writing practical guides on modern web development at Dev98.

Expertise

React
Next.js
JavaScript
TypeScript
Python

Social Media

githublinkedinyoutubewebsite

Related Posts

Micro Frontends
Micro Frontends with Vite and Module Federation
August 08, 2025
1 min
Dev98

Dev98

React · Next.js · Web development