Saga Http Requests

In a worker saga, yield replaces await and put replaces dispatch. Use both to implement the pending/fulfilled/rejected HTTP pattern.

June 28, 20263 min read2 / 2

The worker saga is connected and fires on FETCH_TASKS. Now for the async part: making the HTTP request, handling the response, and dispatching the right action back into Redux state.

[!TIP] Run this yourself: The full worker saga with axios is in the code-practice repo. Run node store.js to see the pending/fulfilled/rejected cycle.

Two Effects: put and yield with a Promise

Two concepts cover everything inside a worker saga.

put is the saga equivalent of dispatch. Instead of calling store.dispatch(action), you yield put(action). The put effect is imported from redux-saga/effects alongside takeEvery.

yield in front of a promise is the saga equivalent of await. Sagas are generators, not async functions, so await is unavailable. yield axios.get(url) hands the promise to the Saga middleware, which resolves it and resumes the generator with the response value.

The mental model:

In a regular async functionIn a worker saga
await fetch(url)yield axios.get(url)
dispatch(action)yield put(action)

The Full Worker Saga

In sagas/tasks.js:

JavaScript
import axios from 'axios'; import { put } from 'redux-saga/effects'; import { FETCH_TASKS_PENDING, FETCH_TASKS_FULFILLED, FETCH_TASKS_REJECTED } from '../actionTypes'; export function* fetchTasksWorker() { yield put({ type: FETCH_TASKS_PENDING }); try { const response = yield axios.get('https://jsonplaceholder.typicode.com/todos'); yield put({ type: FETCH_TASKS_FULFILLED, payload: response }); } catch (error) { yield put({ type: FETCH_TASKS_REJECTED, payload: error }); } }

Three dispatches, three stages: loading, success, failure. The reducer handles each one.

Walking Through the Execution

Line by line:

  1. yield put({ type: FETCH_TASKS_PENDING }) dispatches immediately. The reducer sets loading: true before the request fires.
  2. yield axios.get(url) sends the request and parks the generator. The Saga middleware waits for the promise to resolve.
  3. On success: yield put({ type: FETCH_TASKS_FULFILLED, payload: response }) dispatches the result. The reducer stores the data.
  4. On failure: yield put({ type: FETCH_TASKS_REJECTED, payload: error }) dispatches the error. The reducer stores the error message.

Saga HTTP flow: worker yields put(PENDING), yields axios.get(), on success yields put(FULFILLED), on error yields put(REJECTED) ExpandSaga HTTP flow: worker yields put(PENDING), yields axios.get(), on success yields put(FULFILLED), on error yields put(REJECTED)

How the Result Actions Reach the Reducer

FETCH_TASKS_FULFILLED and FETCH_TASKS_REJECTED are dispatched via put, which sends them through the normal Redux action stream. They pass through the root saga's takeEvery watcher, but takeEvery is only watching for FETCH_TASKS -- not for FETCH_TASKS_FULFILLED. So these result actions flow straight through to the reducer without triggering another worker.

The same pending/fulfilled/rejected pattern from Redux Thunk works here. The difference is that in a Thunk, you dispatch directly. In a Saga, you yield put. The reducer does not know or care which middleware produced the action.

The Essentials

  1. yield put(action) is dispatch inside a saga. Import put from redux-saga/effects and always precede it with yield.
  2. yield promise is await inside a saga. The generator parks while the Saga middleware resolves the promise. The resolved value becomes the return of that yield expression.
  3. FETCH_TASKS_FULFILLED and FETCH_TASKS_REJECTED reach the reducer normally. They are not intercepted by the root saga because their action types do not match the takeEvery pattern.
  4. The pending/fulfilled/rejected pattern is middleware-agnostic. The reducer handles the same three action types regardless of whether Thunk or Saga produced them.

Further Reading and Watching

Practice what you just read.

HTTP Requests in Sagas -- Quiz
1 exercise