Redux-saga: Composing sagas that use selectors

Created on 5 Jul 2016  路  3Comments  路  Source: redux-saga/redux-saga

We're trying to build a bunch of Saga-using components which can be embedded at various different levels of a store hierarchy. For example:

  • An ItemEditor component with it own React view, Redux reducers, and sagas that only deal with the editing of a single item
  • A MetaItemEditor component which embeds an ItemEditors, and also has some additional reducers and sagas to edit some metadata around that item

The sagas in ItemEditor don't know or care that they're inside of MetaItemEditor. In fact, there are other parts of the codebase where we just insert a single ItemEditor all by itself, so this works. But ItemEditor's sagas sometimes need to query the state of its store.

In Saga 0.9.x, we were able to just use getState and override it. We created a helper called combineSagas:

// Combine a collection of sagas into one saga, pass them a selector function
// to pull a relevant piece of state, and then start them all
export const combineSagas = (sagas, selector) => function*(getState) {
    const sagaArray = Array.isArray(sagas) ? sagas : Object.values(sagas);
    const boundSelector = () => selector(getState());
    for (let s of sagaArray){
        yield fork (s, boundSelector);
    }
};

and used it like this:

const watchItemEditor = combineSagas(
    itemEditorSagas, state => state.itemEditorState
);

But 0.10.x has made it more difficult. Now that we have to use select effects, the only way to override the getState function is now doing it directly inside runSaga. So we've ended up having to write our own custom middleware instead of using the built in sagaMiddleware:

// Combine a collection of sagas into one saga, pass them a selector function
// to pull a relevant piece of state, and then start them all
class CombinedSagas {
    constructor(sagas, selector) {
        const sagaArray = Array.isArray(sagas) ? sagas : Object.values(sagas);
        this.generator = function*() {
            for (let s of sagaArray){
                yield fork(s);
            }
        };
        this.selector = getter => () => selector(getter());
    }
}
export const combineSagas = (sagas, selector) =>
    new CombinedSagas(sagas, selector);

// Saga middleware that will correctly process CombinedSagas
export function buildCombinableSagaMiddleware(sagas){
    const sagaArray = Array.isArray(sagas) ? sagas : Object.values(sagas);
    let listeners = new Set();
    // Create middleware that will pull a Redux store
    return ({dispatch, getState}) => {
        // For each saga
        for (let saga of sagaArray) {
            // Check if this is a CombinedSagas
            const isCombined = saga instanceof CombinedSagas;
            // If it is, pull the sagsa from the generator prop
            const generator = isCombined ? saga.generator() : saga();
            const subscribe = (l) => {
                listeners.add(l);
                return () => listeners.delete(l);
            };
            runSaga(generator, {
                subscribe, dispatch,
                // If this is a CombinedSagas, set its getState to the
                // passed in selector; otherwise just give it the entire state
                getState: isCombined ? saga.selector(getState) : getState
            });
        }
        return next => action => {
            const result = next(action);
            listeners.forEach(l => l(action));
            return result;
        };
    };
}

This works, but it's pretty unwieldy. I'm wondering if there's a better way to do this that can still take advantage of the built-in sagaMiddleware.

question

Most helpful comment

Sorry for the late replay.

As you said, overriding select effect is not supported. But I think passing a selector to the child saga would do the same thing

function* megaItemEditorSaga() {
  yield fork(childSaga, itemSelector)
}

function* itemEditorSaga(itemSelector) {
  yield select(itemSelector)
}

All 3 comments

You can pass getState explicitly as argument in sagaMiddleware.run

function* rootSaga(getState) {
  ...
}
const store = createStore(...)
sagaMiddleware.run(rootSaga, store.getState)

Aha, I see. So that would replicate the behavior of getState in 0.9.x.

I guess that works, but I'd prefer to use the new pattern of select effects. The problem, as I see it, is that it's difficult to override where select gets its state from at any level below the root saga. I don't know whether that's a problem with Redux-Saga, or if I've just been following an anti-pattern this whole time.

Sorry for the late replay.

As you said, overriding select effect is not supported. But I think passing a selector to the child saga would do the same thing

function* megaItemEditorSaga() {
  yield fork(childSaga, itemSelector)
}

function* itemEditorSaga(itemSelector) {
  yield select(itemSelector)
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

anthonychung14 picture anthonychung14  路  3Comments

oliversisson picture oliversisson  路  3Comments

tobyl picture tobyl  路  3Comments

andresmijares picture andresmijares  路  3Comments

brunolemos picture brunolemos  路  3Comments