4 Quick Tips for Managing Many Sagas in a React-Redux-Saga App

Redux-Saga

Writing code any other way, once you've embraced the Redux-Saga way of implementing asynchronous operations, is difficult. Everything starts looking like a saga. You add a feature, and another, and another... and, soon, your project has 50+ sagas in it. (I just counted -- my current project has 52 sagas).

But have you ever added a saga and jumped over to your app only to find that your saga doesn't seem to be running? What's going on? webpack --watch is running, the bundle was updated...

One downside to having a boatload of sagas is that each saga has to be registered with the redux-saga middleware in order to run. But fifty-two lines of repetitive code? I don't think there's a JavaScripter alive that wouldn't cry foul over all that 'boilerplate'.

Here's how I managed to manage all those sagas without too much repetition.

First, Arrange By Feature

I have one directory that holds all my saga files and each saga file is composed of a logical grouping of individual sagas.

For instance, I have a userSagas.ts file that contains all my user-related sagas, a sessionSagas.ts file that contains all my session-related sagas, etc.

This makes them easy to find.

list of saga files in Atom editor

Second, Consolidate and Register

In the same saga directory I have an index.ts file that imports all the sagas from the neighbouring saga files and then registers the sagas programmatically. This saves me from having to remember to type out each saga manually.

src/app/sagas/index.ts

import * as buildingUser from './buildingUserSagas';import * as deviceType from './deviceTypeSagas';import * as building from './buildingSagas';import * as session from './sessionSagas';import * as editor from './editorSagas';import * as device from './deviceSagas';import * as layout from './layoutSagas';import * as theme from './themeSagas';import * as user from './userSagas';const sagas = {    ...buildingUser,    ...deviceType,    ...building,    ...session,    ...editor,    ...device,    ...layout,    ...theme,    ...user,};export function registerWithMiddleware(middleware: { run: Function }) {    for (let name in sagas) {        middleware.run(sagas[name]);    }}

All new sagas I write are included automatically because I habitually add the export keyword when defining my sagas. There's no possibility of a saga going unregistered.

I just need to remember to add new saga groups into the index file as I create them, which hasn't been a problem yet.

Then I call the registration function with the saga middleware object from my bootstrapping code (both on the client and the server for universal rendering):

src/client/index.tsx

// Other imports...import createSagaMiddleware from 'redux-saga';import sagas from '../app/sagas';const sagaMiddleware = createSagaMiddleware();const storeWithMiddleware = compose(    applyMiddleware(        sagaMiddleware,        routerMiddleware(browserHistory)    ))(createStore);sagas.registerWithMiddleware(sagaMiddleware);// ...

src/server/webRequestHandler.ts

// Other imports...import createSagaMiddleware from 'redux-saga';import sagas from '../app/sagas';export function webRequestHandler(req: Request, res: Response) {    const sagaMiddleware = createSagaMiddleware();    const store: Store<any> = compose(        applyMiddleware(            sagaMiddleware        )    )(createStore)(appReducer);    sagas.registerWithMiddleware(sagaMiddleware);    // ...

Third, Delete Non Sagas

One of my saga files has a factory method that creates parameterized sagas. But the function itself isn't a saga and, therefore, can't be registered with redux-saga middleware.

JavaScript allows us to delete from an object and, through the magic of webpack, we're just going to be deleting the function from our module's private copy of the import. The function we 'delete' will still exist and is callable from where it needs to be called from.

import * as buildingSagas;// This is not a generator function and it will break// if we try to register it with the middleware.delete buildingSagas.makeBuildingSubRouteSaga;const sagas = {    /* ...all the sagas... */};

This tip isn't for everyone since I've got a special case here.

Note: I don't need to register the parameterized sagas that the factory function produces because they're all subordinate sagas that are only called from higher level sagas.

But this reminds me...

Fourth, Only Export Top-Level Sagas

When you have a hierarchy of sagas (i.e. top-level sagas that call into sub-sagas), you will want to avoid registering the sub-sagas or else you will probably experience unwanted behaviour in your app.

On the other hand, if you need to export sub-sagas for unit testing, then I'd suggest grouping the top-level sagas into an object that gets exported.

For example,

export function* mainSaga1() { /* ... */ }export function* mainSaga2() { /* ... */ }export function* mainSaga3() { /* ... */ }export function* subSaga1() { /* ... */ }export function* subSaga2() { /* ... */ }export function* subSaga3() { /* ... */ }export const sagas = {    mainSaga1, mainSaga2, mainSaga3};

Put these four tips into practice and managing your sagas will be simple. Have you had success with any other tricks? I'd love to hear about it.

Building asynchronous web applications is complicated. React with Redux is not enough. You need something like Redux-Saga to complete the picture.

I can show you how. Sign up on my email list where I write about Redux-Saga and related web development topics.