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.

June 28, 20263 min read1 / 2

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.js to 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:

JavaScript
export const FETCH_TASKS = 'FETCH_TASKS';

In the component, wire up useDispatch:

JSX
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):

JavaScript
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.

Worker saga routing: FETCH_TASKS dispatched, root saga takeEvery routes to fetchTasksWorker, worker saga executes 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:

JavaScript
// 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:

  1. The root saga's takeEvery watcher fires
  2. A new instance of fetchTasksWorker is started
  3. The worker runs and exits
  4. 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

  1. 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.
  2. 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.
  3. The yield before takeEvery in the root saga is required. It parks the root saga at the watcher rather than letting it exit.
  4. The signal action carries no payload. FETCH_TASKS is a trigger. The worker saga decides what to fetch and how.

Further Reading and Watching

Practice what you just read.

Worker Sagas -- Quiz
1 exercise