Redux Saga: How to wait for spawn and call to complete without blocking parent call

Viewed 1206

I'm trying to add a redux saga function but I can't get the chaining right

const randomDelay = () => parseInt(Math.random() * 500)
const a = function*() {
   yield spawn(b)
   yield call(c)
}
const b = function*() {
   yield delay(randomDelay())
}
const c = function*() {
   yield delay(randomDelay())
}
const d = function*() {}
  • I want to call a which will spawn b and call c.
  • When c is complete I want a to become unblocked and complete.
  • When b and c both complete I want to call d

From what I can tell there isn't a way to do this. all or fork will block a

To get around this for now I have c called first and a combo of b and d spawned after but that means b and c can't be running at the same time.

2 Answers

To do this you need a separate signalling mechanism. I would use a channel for this.

  • a first creates a channel
  • a spawns dScheduler passing the channel
  • a passes the channel as an argument to b
  • b does a put to the channel at the end
  • a does a put to the channel at the end (when c finishes)
  • dScheduler does two takes on the channel and then calls d

The code would look something like the following:

import { delay, channel } from "redux-saga";
import { spawn, call, put, take } from "redux-saga/effects";

const randomDelay = () => parseInt(Math.random() * 500);
const B_OR_C_COMPLETED = "B_OR_C_COMPLETED";
export const a = function*() {
  const bcCompletedChannel = channel();
  yield spawn(dScheduler, bcCompletedChannel);
  yield spawn(b, bcCompletedChannel);
  yield call(c);
  yield put(bcCompletedChannel, B_OR_C_COMPLETED);
};
const b = function*(bcCompletedChannel) {
  yield delay(randomDelay());
  yield put(bcCompletedChannel, B_OR_C_COMPLETED);
};
const c = function*() {
  yield delay(randomDelay());
};
const dScheduler = function*(bcCompletedChannel) {
  yield take(bcCompletedChannel);
  yield take(bcCompletedChannel);
  yield call(d);
};
const d = function*() {
};

Here's a CodeSandbox with console logs added and the delay lengthened to make it easy to verify the behavior:

Edit y2yx3rp7rv

The relevant part of the Redux Saga documentation is here. Specifically the section near the bottom called "Using channels to communicate between Sagas".

Not as elegant as https://stackoverflow.com/a/54140525/4453205.

Also my answer assumes a does the job of just calling b and c(or two tasks only).

import { delay } from "redux-saga";
import { all, cancel, put, takeEvery, spawn, call } from "redux-saga/effects";

const randomDelay = () => parseInt(Math.random() * 500);

export function* startTasks() {
  let completed = yield call(a);

  if (completed) {
    yield call(d);
  }
}

const a = function*() {
  let b_finished = false;
  const b = function*() {
    yield delay(randomDelay());
    yield put({ type: "B_DONE" });
    b_finished = true;
  };

  const c = function*() {
    yield delay(randomDelay());
    yield put({ type: "C_DONE" });
  };

  const taskB = yield spawn(b);
  yield call(c);
  yield cancel(taskB);
  return b_finished;
};

const d = function*() {
  yield delay(randomDelay());
  yield put({ type: "D_DONE" });
};

export function* watchStartTasks() {
  yield takeEvery("START_TASKS", startTasks);
}

export default function* rootSaga() {
  yield all([watchStartTasks()]);
}
Related