Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Listening to store change in redux saga

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?

like image 684
user99999 Avatar asked Feb 04 '23 19:02

user99999


1 Answers

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
  }
}
like image 86
cefn Avatar answered Feb 08 '23 17:02

cefn