Redux Saga 101

Feb 15, 2020

I have been working with React and Redux for about 4 years but until now had never used redux-saga. I had a few cursory glances through the documentation but the weird generator syntax put me off. Besides, the alternative, redux-thunk was good enough when it came to handling side effects such as API calls.

But alas, my new company uses redux-saga so it was finally time to learn it. This post is just a collection of my notes and is intended to serve as a reference mainly for myself.

If you’re new to redux-saga, you should probably start with the official docs.

What is redux-saga?

A redux middleware that aims to make application side effects like data fetching easier to manage and test. It’s an alternative to the popular redux-thunk library.

What is a saga?

A saga is like a separate thread in your application that’s solely responsible for side effects.

A saga is written as a generator function that yields plain JS objects known as Effects to the redux-saga middleware. The redux-saga middleware is responsible for dealing with the different Effects (e.g. by dispatching an action to the Redux store).

An example saga

// sagas.ts
import { put, takeEvery, all } from 'redux-saga/effects';

export const delay = ms => new Promise(res => setTimeout(res, ms));

function* helloSaga() {
  console.log('Hello Sagas!');
}

// Our worker Saga: will perform the async increment task
function* incrementAsync() {
  yield call(delay, 1000);
  yield put({ type: 'INCREMENT' });
}

// Our watcher Saga: spawn a new incrementAsync task on each INCREMENT_ASYNC
function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync);
}

// notice how we export the rootSaga single entry point to start all Sagas at once
export default function* rootSaga() {
  yield all([helloSaga(), watchIncrementAsync()]);
}
// store.ts
const sagaMiddleware = createSagaMiddleware();

const store = createStore(reducer, applyMiddleware(sagaMiddleware));

// Run the saga
sagaMiddleware.run(rootSaga);

Testing sagas

One of the supposed advantages of using redux-saga is that it makes testing much easier, at least easier than redux-thunk.

Here’s an example of testing the incrementAsync function from above.

import { call, put } from 'redux-saga/effects';
import { incrementAsync, delay } from '../sagas';

describe('incrementAsync saga', () => {
  const iter = incrementAsync();

  test('calls delay(1000)', () => {
    expect(iter.next().value).toEqual(call(delay, 1000));
  });

  test('dispatches INCREMENT action', () => {
    expect(iter.next().value).toEqual(put({ type: 'INCREMENT' }));
  });

  test('should finish', () => {
    expect(iter.next()).toEqual({ done: true, value: undefined });
  });
});

API reference

takeEvery

Spawns new task every time an action is dispatched.

Example:

yield takeEvery('INCREMENT_ASYNC', incrementAsync);

take

Suspends the generator until an action is dispatched.

Example:

// Waits until ADD_TODO is dispatched
const action = yield take('ADD_TODO');

put

Dispatches an action to the store.

Example:

yield put({ type: 'LOGOUT' });

call

Calls an asynchronous function / generator and waits for it to resolve / return before continuing execution.

Example:

const token = yield call(Api.authorize, user, password);

fork

Starts a task in the background, allowing the function to continue its execution without waiting for the forked task to complete..

Calling yield fork(someTask) returns a Task object which we can then make use of by cancelling it for example.

Example:

yield fork(authorize, user, password);
// Continue execution without waiting for `authorize` task to complete.

cancel

Cancels a task if it’s still running.

Example:

yield cancel(task);

cancelled

Checks if a task has been cancelled.

Example:

function* mySaga() {
  try {
    // ...
  } finally {
    if (yield cancelled()) {
      // logic that should execute only on Cancellation
    }
    // logic that should execute in all situations (e.g. closing a channel)
  }
}

all

Runs multiple Effects in parallel and waits for all of them to complete.

Example:

function* mySaga() {
  const [customers, products] = yield all([
    call(fetchCustomers),
    call(fetchProducts)
  ]);
}

select

Uses a selector function to extract a slice of the store state.

Note: The selector function will run on the state after an action has been handled by the reducers.

Example:

// getPosts would be a selector
const posts = yield select(getPosts)