Using redux-saga with setInterval - how and when to yield

dougajmcdonald picture dougajmcdonald · Dec 31, 2017 · Viewed 8.1k times · Source

Having just moved from thunks to sagas I'm trying to find the best way to call setTimeout and then from within that function call another function (in this case corewar.step()). This was my original code which works as I'd expect.

  runner = window.setInterval(() => {

    for(let i = 0; i < processRate; i++) {
      corewar.step()
    }

    operations += processRate;

  }, 1000/60)

This code is inside a saga and I believe that I should be able to wrap function calls within call as I've done in other areas in the application.

I've tried wrapping the setInterval call in a call and leaving everything else as it is, which results in step() never being called.

  runner = yield call(window.setInterval, () => {

    for(let i = 0; i < processRate; i++) {
      corewar.step()
    }

    operations += processRate;

  }, 1000/60)

I've tried, leaving the setInterval as it is and wrapping the step() function in a call and changing the anonymous function signature to function* which also results in step() never being called.

  runner = window.setInterval(function*() {

    for(let i = 0; i < processRate; i++) {
      yield call([corewar, corewar.step])
    }

    operations += processRate;

  }, 1000/60)

Finally, I've tried wrapping both, which again results in step() never being called.

  runner = yield call(window.setInterval, function*() {

    for(let i = 0; i < processRate; i++) {
      yield call([corewar, corewar.step])
    }

    operations += processRate;

  }, 1000/60)

It feels like I'm missing something here so my question is, should I need to wrap these functions up in call at all or is this wrong?

The follow on question if I am supposed to wrap the outer setInterval in a call would be how should I be defining a function as a parameter to call which also wants to yield either a put or call itself?

Answer

madc picture madc · Mar 15, 2018

There is a section in the saga-redux docs called "Using the eventChannel factory to connect to external events", that suggests using channels.

This section is also providing an example for a setInterval implementation:

import { eventChannel, END } from 'redux-saga'

function countdown(secs) {
  return eventChannel(emitter => {
      const iv = setInterval(() => {
        secs -= 1
        if (secs > 0) {
          emitter(secs)
        } else {
          // this causes the channel to close
          emitter(END)
        }
      }, 1000);
      // The subscriber must return an unsubscribe function
      return () => {
        clearInterval(iv)
      }
    }
  )
}

You would then use yield call and yield takeEvery to set it up:

const channel = yield call(countdown, 10);
yield takeEvery(channel, function* (secs) {
    // Do your magic..
});