Is it safe to call sagaMiddleware.run multiple times?

Viewed 834

I'm using redux and redux-saga in an application to manage state and asynchronous actions. In order to make my life easier, I wrote a class that acts essentially as a saga manager, with a method that "registers" a saga. This register method forks the new saga and combines it with all other registered sagas using redux-saga/effects/all:

class SagasManager {
    public registerSaga = (saga: any) => {
        this._sagas.push(fork(saga));
        this._combined = all(this._sagas);
    }
}

This class is then used by my store to get the _combined saga, supposedly after all sagas are registered:

const store = Redux.createStore(
    reducer, 
    initialState, 
    compose(Redux.applyMiddleware(sagaMiddleware, otherMiddleware)),
);
sagaMiddleware.run(sagasManager.getSaga());

However, I ran into the problem that depending on circumstances (like import order), this doesn't always work as intended. What was happening was that some of the sagas weren't getting registered before the call to sagaMiddleware.run.

I worked around this by providing a callback on SagasManager:

class SagasManager {
    public registerSaga = (saga: any) => {
        this._sagas.push(fork(saga));
        this._combined = all(this._sagas);
        this.onSagaRegister();
    }
}

And then the store code can use this as

sagasManager.onSagaRegister = () => sagaMiddleware.run(sagasManager.getSaga());

This seems to work, but I can't find in the docs whether this is safe. I did see that .run returns a Task, which has methods for canceling and the like, but since my problem is only in that awkward time between when the store is constructed and the application is rendered I don't that would be an issue.

Can anyone explain whether this is safe, and if not what a better solution would be?

1 Answers

It may depend on what you mean by "safe". What exactly do you mean by that in this case?

First, here's the source of runSaga itself, and where it gets used by the saga middleware.

Looking inside runSaga, I see:

export function runSaga(options, saga, ...args) {
  const iterator = saga(...args)

  // skip a bunch of code

  const env = {
    stdChannel: channel,
    dispatch: wrapSagaDispatch(dispatch),
    getState,
    sagaMonitor,
    logError,
    onError,
    finalizeRunEffect,
  }

  const task = proc(env, iterator, context, effectId, getMetaInfo(saga), null)

  if (sagaMonitor) {
    sagaMonitor.effectResolved(effectId, task)
  }

  return task
}

What I'm getting out of that is that nothing "destructive" will happen when you call runSaga(mySagaFunction). However, if you call runSaga() with the same saga function multiple times, it seems like you'll probably have multiple copies of that saga running, which could result in behavior your app doesn't want.

You may want to try experimenting with this. For example, what happens if you have a counter app, and do this?

function* doIncrement() {
    yield take("DO_INCREMENT");
    put({type : "INCREMENT"});
}

sagaMiddleware.runSaga(doIncrement);
sagaMiddleware.runSaga(doIncrement);

store.dispatch({type : "DO_INCREMENT"});

console.log(store.getState().counter);
// what's the value?

My guess is that the counter would be 2, because both copies of doIncrement would have responded.

If that sort of behavior is a concern, then you probably want to make sure that prior sagas are canceled.

I actually ran across a recipe for canceling sagas during hot-reloading a while back, and included a version of that in a gist for my own usage. You might want to refer to that for ideas.

Related