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
}
}
anotherPartOfState
in a reducer? Then you can justtakeLatest
this specific action. – Dorothy