Worker Saga
Worker sagas are generator functions that handle specific action types. takeEvery maps an action type to a worker, which runs on every matching dispatch.
The root saga is wired and running. But an empty root saga with no watchers exits immediately and warns. The missing piece is the mapping: which action types should trigger which worker sagas.
[!TIP] Run this yourself: Worker saga setup is in the code-practice repo. Run
node store.jsto see the takeEvery watcher start and the worker saga fire.
What Is a Worker Saga?
A worker saga is a generator function responsible for handling one specific action type. Where the root saga stays alive and watches the action stream, a worker saga runs once per matching dispatch, does its async work, and exits.
The split is deliberate. Root saga = permanent watcher. Worker saga = one-time handler. This keeps each concern in its own function.
Creating an Action Type and Dispatcher
Before the saga can respond to anything, there must be an action to dispatch. In actionTypes.js:
export const FETCH_TASKS = 'FETCH_TASKS';In the component, wire up useDispatch:
import { useDispatch, useSelector } from 'react-redux';
import { FETCH_TASKS } from '../actionTypes';
function TaskList() {
const dispatch = useDispatch();
const tasks = useSelector(state => state.tasks);
return (
<div>
<button onClick={() => dispatch({ type: FETCH_TASKS })}>
Load Tasks
</button>
{/* render tasks */}
</div>
);
}FETCH_TASKS has no payload -- it is a signal. The saga intercepts it and decides what to do next.
The takeEvery Effect
takeEvery is a saga effect imported from redux-saga/effects. It takes an action type and a worker saga, then watches the action stream. Every time an action matching that type is dispatched, takeEvery invokes the worker saga.
In sagas/index.js (the root saga):
import { takeEvery } from 'redux-saga/effects';
import { FETCH_TASKS } from '../actionTypes';
import { fetchTasksWorker } from './tasks';
export function* rootSaga() {
yield takeEvery(FETCH_TASKS, fetchTasksWorker);
}The yield before takeEvery is what keeps the root saga alive. Without it, the function returns immediately after running takeEvery and the watcher dies. With yield, the root saga parks here and resumes on every matching dispatch.
ExpandWorker saga routing: FETCH_TASKS dispatched, root saga takeEvery routes to fetchTasksWorker, worker saga executes
Creating the Worker Saga File
Create sagas/tasks.js alongside the root saga:
// src/sagas/tasks.js
export function* fetchTasksWorker() {
console.log('fetchTasksWorker invoked');
// HTTP request comes in the next post
}This is a generator function with no yield yet -- a stub that confirms the routing works. Dispatching FETCH_TASKS from the component should log the message. If it does, takeEvery is correctly wired.
The Separation Between Root and Worker
The root saga is responsible for routing. The worker saga is responsible for work. They are separate files and separate concerns.
When FETCH_TASKS is dispatched:
- The root saga's
takeEverywatcher fires - A new instance of
fetchTasksWorkeris started - The worker runs and exits
- The root saga keeps watching for the next dispatch
takeEvery allows concurrent workers. If FETCH_TASKS is dispatched three times in quick succession, three worker instances run in parallel. The alternative, takeLatest, cancels the previous instance before starting the new one.
The next post fills in the body of fetchTasksWorker -- the yield axios.get() that fires the request and the yield put() calls that dispatch the result back into Redux state.
The Essentials
- Worker sagas handle one action type each. The root saga routes; the worker works. Keeping them separate makes both functions single-purpose and easy to test.
takeEvery(actionType, workerFn)maps an action type to a worker. Every matching dispatch starts a new worker instance. Multiple concurrent dispatches produce multiple concurrent workers.- The
yieldbeforetakeEveryin the root saga is required. It parks the root saga at the watcher rather than letting it exit. - The signal action carries no payload.
FETCH_TASKSis a trigger. The worker saga decides what to fetch and how.
Further Reading and Watching
- Live React: Hot Reloading with Time Travel (Dan Abramov, ReactEurope 2015): the talk that demonstrated how Redux middleware intercepts the action stream -- the exact mechanism
takeEverybuilds on - redux-saga/effects API reference: full
takeEverydocs covering pattern matching, action forwarding, and the difference fromtakeLatest
Practice what you just read.
Keep reading