Listening to store change in redux saga
Asked Answered
D

2

7

I'm trying to create a redux saga that will listen to a change for one variable in the state. When it does change, I want to dispatch some other action. Is this possible?

This is what I want to do:

yield takeLatest(fooAction, fetchAll);

function* fetchAll() {
   const part = yield select(getPartOfState);
   if (part.flag) {
      yield call(listenToChange);
   }
}

function* listenToChange() {
   const anotherPart = yield select(getAnotherPartOfState);
   if (anotherPart === true) { // this is what I want to wait for
      // do something
   }
}

So I basically want to wait for anotherPart to change, because initially it will be false, and execute this in the loop just once (even if the listenToChange gets executed multiple times. Is this possible?

Diabolism answered 28/3, 2017 at 14:50 Comment(1)
Theoretically there are different ways to do this, but I don't think listening to changed state in sagas is in any way idiomatic. Don't you have an action that is responsible for triggering a change of anotherPartOfState in a reducer? Then you can just takeLatest this specific action.Dorothy
C
7

I adopted the pattern below, which does exactly what you describe.

It works by waiting on every action passing through the store, and repeats a selector to see if a specific value has changed, triggering a saga.

The signature is a wrapping function, which enables you to pass a selector and a saga. The saga has to accept previous and next values. The wrapping function 'hands over' to your saga once for every change in the selected value. You should author logic in your saga to 'take over' from the wrapping generator using the normal yield calls, when relevant conditions are met.

import { take, spawn, select } from "redux-saga/effects"

function* selectorChangeSaga(selector, saga) {
  let previous = yield select(selector)
  while (true) {
    const action = yield take()
    const next = yield select(selector)
    if (next !== previous) {
      yield* saga(next, previous)
      previous = next
    }
  }
}

Below is a tested example which defines a saga in my application. It generates a normal saga, run in the normal way.

The logic runs whenever the state's "focusId" value changes. My sagas carry out the lazy-loading of remote data corresponding with the id, and opportunistically refresh the lists from a server. Note the asterisks, especially the yield * delegating yield ! It defines how the generators 'hand off' to each other.

//load row when non-null id comes into focus  
function* focusIdSaga() {
  yield* selectorChangeSaga(state => state.focusId, function* (focusId, prevFocusId) {
    const { focusType, rows } = yield select()
    if (focusType) {
      if (!prevFocusId) { //focusId previously new row (null id)
        //ensure id list is refreshed to include saved row
        yield spawn(loadIdsSaga, focusType)
      }
      if (focusId) { //newly focused row
        if (!rows[focusId]) {
          //ensure it's loaded
          yield spawn(loadRowSaga, focusType, focusId)
        }
      }
    }
  })
}

By contrast with @alex and @vonD I am personally comfortable monitoring state, and I feel it performs adequately and offers a terse and reliable way not to miss the change you care about without unnecessary indirection. If you only track actions, it is easy to introduce bugs by creating an action which changes state, while not remembering to add the action type to your filter. However, if you consider performance of the repeated selector to be an issue, you can narrow the filter of the 'take' in order to only respond to certain actions which you KNOW to affect the part of the state tree you are monitoring.

UPDATE

Building on the approach shown by @vonD I have refactored the example above in a way which is a bit cleaner. The monitorSelector() function interacts with the conventional yield-based flow of a saga without wrapping anything. It provides a way for a saga to 'block' to wait for a changing value.

function* monitorSelector(selector, previousValue, takePattern = "*") {
  while (true) {
    const nextValue = yield select(selector)
    if (nextValue !== previousValue) {
      return nextValue
    }
    yield take(takePattern)
  }
}

This is the tested version of the saga from the original example, but refactored for the new way of monitoring state.

//load row when non-null id comes into focus  
function* focusIdSaga() {
  let previousFocusId
  while (true) {
    const focusId = yield* monitorSelector(state => state.focusId, previousFocusId)
    const { focusType, rows } = yield select()
    if (focusType) {
      if (!previousFocusId) { //focusId previously new row (null id)
        //ensure id list is refreshed to include saved row
        yield spawn(loadIdsSaga, focusType)
      }
      if (focusId) { //newly focused row
        if (!rows[focusId]) {
          //ensure it's loaded
          yield spawn(loadRowSaga, focusType, focusId)
        }
      }
    }
    previousFocusId = focusId
  }
}
Catalpa answered 6/10, 2019 at 7:28 Comment(0)
S
3

As Alex mentioned in his comment, listening to a state change comes down to listening to the actions that are likely to trigger a change of this piece of state.

The take effect can take various patterns describing actions as a parameter, which can help you do just that: an action, an array of actions, a function, etc.. If you don't want to whitelist such actions, you can even call take without an argument (or with the string '*' if you want to be more explicit), which gives you a chance to inspect state after every action.

With this is mind, a saga waiting for a piece of state to have a given value could be written like this:

function *waitForStateToHaveValue(selector, expectedValue) {
  let stateSlice = yield select(selector);
  while (stateSlice !== expectedValue) {
    yield take();
    stateSlice = yield select(selector);
  }
}
Sulfathiazole answered 3/4, 2017 at 9:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.