redux-saga: Saga triggered in yield map loop only executes once

Really enjoying redux-saga, but I’m running into a problem while using yield myArray.map. I have simplified the sagas from my app to illustrate:

export function * listCamerasFlow () {
  while (true) {
    const {deviceId} = yield take(CAMERA_LIST_REQUEST)
    let cameras = [{ id: '1', deviceId }, { id: '2', deviceId }]
    yield put(cameraListReady(cameras))
    yield cameras.map(camera => put(cameraSnapshotRequest(camera.deviceId, camera.id)))
    console.log('CAMERAS', cameras)
  }
}

export function * cameraSnapshotFlow () {
  while (true) {
    const request = yield take(CAMERA_SNAPSHOT_REQUEST)
    console.log('GOT REQUEST', request)
    // yield put(cameraSnapshotReady('yo'))
    console.log('DONE')
  }
}

export function * root () {
  yield fork(listCamerasFlow)
  yield fork(cameraSnapshotFlow)
}

And here are the action creators:

export const cameraSnapshotRequest = (deviceId, cameraId) => {
  console.log('ACTION', cameraId, deviceId)
  return {
    type: CAMERA_SNAPSHOT_REQUEST,
    deviceId,
    cameraId
  }
}

export const cameraSnapshotReady = (imageURL) => ({
  type: CAMERA_SNAPSHOT_READY,
  imageURL
})

When CAMERA_LIST_REQUEST comes in, the following is logged to the console. Note that GOT REQUEST and DONE are logged twice (as expected):

ACTION 1 644ffd5d-ee45-45dd-95ef-7efc5b6218f1
ACTION 2 644ffd5d-ee45-45dd-95ef-7efc5b6218f1
GOT REQUEST Object {type: "app/CAMERA_SNAPSHOT_REQUEST", deviceId: "644ffd5d-ee45-45dd-95ef-7efc5b6218f1", cameraId: "1"}
DONE
GOT REQUEST Object {type: "app/CAMERA_SNAPSHOT_REQUEST", deviceId: "644ffd5d-ee45-45dd-95ef-7efc5b6218f1", cameraId: "2"}
DONE
CAMERAS [Object, Object]

However, if I uncomment yield put(cameraSnapshotReady('yo'):

export function * cameraSnapshotFlow () {
  while (true) {
    const request = yield take(CAMERA_SNAPSHOT_REQUEST)
    console.log('GOT REQUEST', request)
    yield put(cameraSnapshotReady('yo'))   // <---- added this line
    console.log('DONE')
  }
}

I now get the following logged to the console:

ACTION 1 644ffd5d-ee45-45dd-95ef-7efc5b6218f1
ACTION 2 644ffd5d-ee45-45dd-95ef-7efc5b6218f1
GOT REQUEST Object {type: "app/CAMERA_SNAPSHOT_REQUEST", deviceId: "644ffd5d-ee45-45dd-95ef-7efc5b6218f1", cameraId: "1"}
CAMERAS [Object, Object]
DONE

Notice that GOT REQUEST and DONE are only logged one time. The second put within yield cameras.map is called since ACTION 1 shows up. But cameraSnapshotFlow seems to only get one take. Also, DONE comes after CAMERAS in the second scenario.

Is there something simple I’m doing wrong? Any ideas why this is not working?

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Comments: 15 (11 by maintainers)

Most upvoted comments

I marked this as a bug. I’ll try to ‘fix’ the scheduler to make it handle this case (maybe possible). Otherwise we have to document this case and possible workarounds. but I prefer to delay the 2nd solution until we make sure this can’t be fixed in the scheduler.

I was under the impression that using map (yield cameras.map(…)) would allow the requests to be run concurrently. But they seem to run in sequence,

Since JavaScript is a single threaded language, everything have to run in sequence. But redux-saga schedules puts internally by ‘executing’ them one after another. ‘executing’ means that when we execute a put, we dont allow another put to execute until we exhaust the effects of the currently executing put.

In your case, when you yield the array of puts, the first put in the array gets executed atomically (i.e. will delay other puts yielded during its execution). Since your cameraSnapshotFlow is issuing a put itself in the middle of the execution of the 1st listCamerasFlow ...-> cameras.map(...) put, it will be delayed until all pending puts execute. This means the cameraSnapshotFlow will be suspended until the puts of the cameras.map(...) array execute. which means it can’t take the 2nd put.

Why do we delay the cameraSnapshotFlow’s put? Imagine listCamerasFlow want to take actions from cameraSnapshotFlow

function * listCamerasFlow () {
  while (true) {
    const {deviceId} = yield take(CAMERA_LIST_REQUEST)
    let cameras = [{ id: '1', deviceId }, { id: '2', deviceId }] 
    yield put(cameraSnapshotRequest(deviceId, cameras[0].id)))
    // here
    yield take(CAMERA_SNAPSHOT_READY)
    console.log('CAMERAS', cameras)
  }
}

when listCamerasFlow executes the put effect. The cameraSnapshotFlow reaction will execute inside the same stack frame of the put. it means when we resume to yield take(CAMERA_SNAPSHOT_READY) the CAMERA_SNAPSHOT_READY action has already been dispatched when we were executing the previous put effect. redux-saga delays nested put effects in order to make it possible for them to be taken later like in this case.

Of course in your case listCamerasFlow is not interested in taking anything from cameraSnapshotFlow but the presence/absence of an interdependence can not be inferred by the library.

Yes, actionChannel is the correct way to solve the problem or takeEvery (depending on a use case). puts are scheduled with internal scheduler so nested tasks etc are handled correctly.

The problem lies here that when 1st put is being handled take reacts to it, meets put which is queued internally, not resolved immediately so take could be met again, so in the moment of 2nd put (from the map) the worker (saga) you wanted to w8 for the dispatched action didnt come to its take again.