Q&A
What is Redux-Saga?
Overview
Redux reducers must be pure - they can't do async work. Redux-Saga is one solution for side effects (the other common one is Redux-Thunk). A saga is a long-running process, written as a generator, that listens for actions and orchestrates side effects (fetching data, then dispatching success/failure actions). Generators let the middleware pause/resume the flow, which makes complex async logic readable and highly testable.
graph LR
A[Component dispatches action] --> B[Saga watcher: takeEvery/takeLatest]
B --> C[Worker saga runs]
C --> D["call(api) - perform async effect"]
D --> E["put(successAction) - dispatch result"]
E --> F[Reducer updates store]
style C fill:#1e1b4b,color:#fff
Why Generators?
A saga is a generator function (function*) that yields effect descriptions (plain objects describing what to do) rather than performing them directly. The middleware executes those effects and resumes the generator with the result. Because the saga only describes effects, it's easy to test (assert on the yielded objects) and the middleware can cancel a paused saga.
A Typical Saga
import { call, put, takeLatest } from "redux-saga/effects";
import api from "./api";
// worker saga: handles one fetch-user request
function* fetchUser(action) {
try {
const user = yield call(api.getUser, action.payload.id); // async call
yield put({ type: "USER_FETCH_SUCCEEDED", user }); // dispatch success
} catch (e) {
yield put({ type: "USER_FETCH_FAILED", message: e.message });
}
}
// watcher saga: listens for the action and runs the worker
function* watchFetchUser() {
yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
A component just dispatches { type: "USER_FETCH_REQUESTED", payload: { id } }; the saga handles the async flow and dispatches the outcome.
Core Effects
| Effect | Purpose |
|---|---|
call(fn, ...args) |
Invoke a function (often returning a promise) and wait for it |
put(action) |
Dispatch an action to the store |
take(pattern) |
Wait for a specific action to be dispatched |
takeEvery(pattern, saga) |
Run the saga on every matching action (concurrent) |
takeLatest(pattern, saga) |
Run on the latest, cancelling previous in-flight runs |
select(selector) |
Read from the store state |
fork / all |
Run sagas concurrently (non-blocking / in parallel) |
cancel / cancelled |
Cancel tasks / handle cancellation |
takeEvery vs takeLatest
takeEvery- handles every dispatched action, allowing concurrent runs.takeLatest- if a new matching action arrives while a previous handler is still running, it cancels the old one and only keeps the latest result. This neatly solves the stale-response race condition (e.g. search-as-you-type) for free - a headline benefit of sagas.
Redux-Saga vs Redux-Thunk
| Redux-Saga | Redux-Thunk | |
|---|---|---|
| Mechanism | Generators + effects | Functions returning functions |
| Complexity | Steeper learning curve | Simple, minimal |
| Best for | Complex async flows, cancellation, concurrency | Simple async (most apps) |
| Testability | Very high (yielded effects are data) | Moderate |
| Cancellation | Built-in (takeLatest, cancel) |
Manual |
Thunk is simpler and fine for most apps; Saga shines when you have complex orchestration, need cancellation/debouncing, or want highly testable effect logic.
Setup
import createSagaMiddleware from "redux-saga";
const sagaMiddleware = createSagaMiddleware();
const store = configureStore({ reducer, middleware: () => [sagaMiddleware] });
sagaMiddleware.run(rootSaga);
Note on Modern Redux
Redux Toolkit's RTK Query and createAsyncThunk now cover much async/data-fetching, so new projects may not need Saga. But Saga remains valuable for complex, long-lived, or highly-orchestrated side effects - and appears in many existing codebases.
Why This Matters in Interviews
Redux interviews use Saga to test side-effect management. Key points: it handles side effects outside pure reducers using generators that yield effect descriptions (declarative, testable, cancelable); the watcher/worker pattern; core effects (call, put, takeEvery vs takeLatest); and the Saga vs Thunk trade-off (power/cancellation/testability vs simplicity). The takeLatest race-condition solution and high testability (effects are plain data) are the standout selling points.
Common Pitfalls
- Reaching for Saga when Thunk/RTK Query would be simpler (over-engineering).
- Putting side effects in reducers (they must stay pure - that's why Saga exists).
- Using
takeEverywheretakeLatestis needed (stale-response races). - Forgetting
sagaMiddleware.run(rootSaga)so sagas never start. - Misunderstanding that
yield call(...)describes the call; the middleware executes it.
References
- Redux-Saga docs: https://redux-saga.js.org/
- Redux side effects overview: https://redux.js.org/usage/side-effects-approaches