37

redux-saga project has been existing for a pretty long time now, but still there are a lot of confusing things about this library. And one of them is: how to start your rootSaga. For example, in the beginner tutorial rootSaga is started by yeilding an array of sagas. Like this

export default function* rootSaga() {
  yield [
    helloSaga(),
    watchIncrementAsync()
  ]
}

However, in the using saga helpers section rootSaga consists of two forked sagas. Like this:

export default function* rootSaga() {
  yield fork(watchFetchUsers)
  yield fork(watchCreateUser)
}

The same way of starting rootSaga is used in async example in redux-saga repo. However, if you check real-world and shopping-card examples, you'll see that rootSagas there yeild an array of forked sagas. Like this:

export default function* root() {
  yield [
    fork(getAllProducts),
    fork(watchGetProducts),
    fork(watchCheckout)
  ]
}

Also, if you read some discussions in redux-saga issues, you'll see that some people suggest to use spawn instead of fork for rootSaga to guard you application from complete crashing if one of your forked sagas is canceled because of some unhandled exception.

So, which way is the most right way to start your rootSaga? And what are the differences between the existing ones?

slava shkodin
  • 479
  • 4
  • 5

2 Answers2

17

You can start multiple root sagas. But any saga has the ability to start another saga on its own. Thus it's possible to start a single root saga, that creates the other sagas.

You just need to be aware of how errors propagate to the parent saga. If you have a single root saga and a child saga crashed, by default the error will propagate to the parent which will terminate, which will also kill all the other sagas started from this parent.

It's up to you to decide this behavior. According to your application you may want to have a fail fast behavior (make the whole app unusable if there's such a problem), or fail safe, and try to make the app continue working even if some parts may have problems.

Generally I'd recommend that you start multiple root sagas, or your parent saga uses spawn instead of fork so that your app remains usable if there's a crash. Note that it's also quite easy to forget to catch errors in some places. You generally don't want, for example, to have all your app become unusable if there's a single API request that fails

Edit: I'd recommend to take a look at https://github.com/yelouafi/redux-saga/issues/570

In this redux-saga issue, I show different ways to start sagas and the impact it has on your application.

TLDR: this is how I usually start root sagas:

const makeRestartable = (saga) => {
  return function* () {
    yield spawn(function* () {
      while (true) {
        try {
          yield call(saga);
          console.error("unexpected root saga termination. The root sagas are supposed to be sagas that live during the whole app lifetime!",saga);
        } catch (e) {
          console.error("Saga error, the saga will be restarted",e);
        }
        yield delay(1000); // Workaround to avoid infinite error loops
      }
    })
  };
};

const rootSagas = [
  domain1saga,
  domain2saga,
  domain3saga,
].map(makeRestartable);

export default function* root() {
  yield rootSagas.map(saga => call(saga));
}
Sebastien Lorber
  • 79,294
  • 59
  • 260
  • 386
  • 1
    Hey, Sebastien, thx for the answer. I was also thinking about using spawn instead of fork for sagas, that rootSaga consists of. But could you also give me an answer about the differences between yielding an array of sagas, yielding several forked sagas and yielding an array of forked sagas. Because, as you can see from my question, all three ways are used in redux-saga docs and examples to start a rootSaga. And that's super confusing) – slava shkodin Sep 12 '16 at 10:28
  • 1
    honnestly I don't think there's much difference. If you yield an array of forked sagas, each fork gives you a promise and you basically do a race effect over the promise array. If one promise fails the race effect fails and unless parent handles the error the forks will also be cancelled. If you yield generators calls in an array it's almost the same, except the race effect will cancel the generator calls directly. Forking multiple sagas might give you better concurrency control in case you need to use things like `join` – Sebastien Lorber Sep 12 '16 at 10:52
  • Thank you, for the clarification. Not completely sure, what do you mean by "cancel the generator calls directly". – slava shkodin Sep 12 '16 at 11:53
  • I mean that redux-saga will have generators inside the array, so it has the ability to directly cancel the generator tasks. If it only has promises in the array, it can't cancel a promise because std promises are not (yet) cancellable. So it can only throw an error on the race effect, which will (likely) terminate the parent, and redux-saga will kill the forked childs. In the end the result is the same, it's just taking different code paths to achieve it. – Sebastien Lorber Sep 12 '16 at 13:25
  • hmmmm oh wait I think I'm melting between yielding an array and the race effect :D so to take your example, I'd say 1 and 3 are almost the same and they block on the yielded array, while 2 is quite similar but does not block (so you might be able to add some code under your forkings). There's no right way to start your sagas, you just need to be aware of the behavior under failure and choose wisely for your app. But at the root, for totally unrelated sagas, I'd recommend using spawn instead of fork. – Sebastien Lorber Sep 12 '16 at 13:40
16

How to create rootSaga?

According to a core developer of redux-saga [1,2] the idiomatic way to create rootSaga is to use the all Effect Combinator. Also, please note that yielding arrays from sagas is deprecated.

Example 1

You could use something like this (+all)

import { fork, all } from 'redux-saga/effects';
import firstSaga from './firstSaga';
import secondSaga from './secondSaga';
import thirdSaga from './thirdSaga';

export default function* rootSaga() {
    yield all([
        fork(firstSaga),
        fork(secondSaga),
        fork(thirdSaga),
    ]);
}

Example 2

Taken from here

// foo.js
import { takeEvery } from 'redux-saga/effects';
export const fooSagas = [
  takeEvery("FOO_A", fooASaga),
  takeEvery("FOO_B", fooBSaga),
]

// bar.js
import { takeEvery } from 'redux-saga/effects';
export const barSagas = [
  takeEvery("BAR_A", barASaga),
  takeEvery("BAR_B", barBSaga),
];

// index.js
import { fooSagas } from './foo';
import { barSagas } from './bar';

export default function* rootSaga() {
  yield all([
    ...fooSagas,
    ...barSagas
  ])
}

fork vs. spawn

fork and spawn will both return Task objects. Forked tasks are attached to parent, whereas spawned tasks are detached from the parent.

  • Error handling in forks [link]:

    Errors from child tasks automatically bubble up to their parents. If any forked task raises an uncaught error, then the parent task will abort with the child Error, and the whole Parent's execution tree (i.e. forked tasks + the main task represented by the parent's body if it's still running) will be cancelled.

  • Error handling in spawned tasks [link]:

    The parent will not wait for detached tasks to terminate before returning and all events which may affect the parent or the detached task are completely independent (error, cancellation).

Based on above, you could, use fork for "mission critical" tasks, i.e. "if this task fails, please crash the whole app", and spawn for "not critical" tasks, i.e. "if this task fails, do not propagate the error to the parent".

np8
  • 14,736
  • 8
  • 50
  • 67