Jonas Sourlier
Jonas Sourlier

Reputation: 14435

Redux Saga seems to block execution even though the saga is started with `spawn`

I'm using the following setup in my React Native app:

  1. The GUI uses connect from react-redux to fire an action, let's say of type MY_ACTION. The action creator is a function which sits on the GUI's props (connect puts it there). So, the GUI calls this.props.sendMyAction() to dispatch.
  2. In my root saga, I have the following dispatcher, which listens for MY_ACTION and invokes a background process every time a MY_ACTION is dispatched:
    function* myActionWatcher() {
        yield takeEvery(MY_ACTION, function*() {
            yield spawn(backgroundSaga);
        });
    }

This seems to work. However, the JS thread seems to return to the GUI only when the background saga has completed:

this.props.sendMyAction();
console.info('Sent action!');              // <---- only called after backgroundSaga finishes

This seems strange, given that spawn is exactly the kind of forking mechanism that forks the saga in an asynchronous way, i.e. such that the parent saga does not have to wait until it's finished.

What am I missing?

Upvotes: 0

Views: 1188

Answers (1)

Nicholas Tower
Nicholas Tower

Reputation: 84912

What does backgroundSaga do? If it starts off with a synchronous block of code, that code will have to finish executing before control can return back to the parent saga, or to redux, or to your component.

When you call yield spawn(backgroundSaga), you create a detached saga and begin running that saga. The saga will run until it yields something asynchronous (usually a promise). The fact that the child saga is detached means it's possible for the parent saga to finish even if the child saga hasn't, but the parent still needs to wait for the child to yield. Only once the child saga yields does control return back to the parent saga, and then back to redux, and then back to your react component.

For example, suppose i have the following:

function* myActionWatcher() {
  yield takeEvery(MY_ACTION, function*() {
    console.log('PARENT: about to spawn');
    yield spawn(backgroundSaga);
    console.log('PARENT: after spawn');
  });
}


function* backgroundSaga() {
  console.log('CHILD: starting saga');
  const state = yield select();
  console.log('CHILD: after select');
  yield Promise.resolve();
  console.log('CHILD: after resolve');
}

The order of logs for this will be:

  • 'PARENT: about to spawn'
  • 'CHILD: starting saga'
  • 'CHILD: after select'
  • 'PARENT: after spawn'
  • 'Sent action!' <-- from your component
  • 'CHILD: after resolve'

In other words, the child starts running immediately, and anything synchronous it does (including synchronous yields to redux-saga for information) run to completion. Then once it reaches something asynchronous control returns to the parent, and the child will resume later.

Upvotes: 1

Related Questions