A Redux + redux-saga Wiring I Still Reach For

redux-saga still earns its keep when async workflows get tangled. The wiring I copy-paste into legacy codebases, plus the takeEvery vs takeLatest decision and the canonical fetch saga.

JavaScript
Frontend
3 snippets
react
saga-pattern
async-await
felixhaddad

By @felixhaddad

May 11, 2026

·

Updated May 20, 2026

1,138 views

21

Rate

// The bootstrap. createSagaMiddleware returns the middleware AND a .run
// method you call after applyMiddleware so the root saga is registered
// against the live store. Important: .run must come AFTER createStore;
// calling it before is the most common bootstrap bug.

// Tiny stand-ins so the snippet runs in the playground.
function applyMiddleware() {
    const mws = Array.prototype.slice.call(arguments);
    return function enhancer(createStoreFn) {
        return function (reducer, initialState) {
            const store = createStoreFn(reducer, initialState);
            const chain = mws.map((mw) => mw({ getState: store.getState, dispatch: (a) => store.dispatch(a) }));
            const composedDispatch = chain.reduceRight(
                (next, mw) => mw(next),
                store.dispatch
            );
            return Object.assign({}, store, { dispatch: composedDispatch });
        };
    };
}

function createStoreBase(reducer, initialState) {
    let state = initialState;
    const listeners = [];
    return {
        getState: () => state,
        dispatch: (action) => { state = reducer(state, action); listeners.forEach((l) => l()); return action; },
        subscribe: (l) => { listeners.push(l); },
    };
}

function createStore(reducer, initialState, enhancer) {
    return enhancer ? enhancer(createStoreBase)(reducer, initialState) : createStoreBase(reducer, initialState);
}

// createSagaMiddleware stand-in: collects dispatched actions and routes them
// to whatever generator the user .run's. The real one wires up the channel,
// effect runtime, and error boundary; the wiring shape is the same.
function createSagaMiddleware() {
    let runningSaga = null;
    let storeApi = null;
    const middleware = function (api) {
        storeApi = api;
        return function (next) {
            return function (action) {
                const result = next(action);
                if (runningSaga && typeof runningSaga.next === 'function') runningSaga.next(action);
                return result;
            };
        };
    };
    middleware.run = function (saga) {
        runningSaga = saga(storeApi);
        if (runningSaga && typeof runningSaga.next === 'function') runningSaga.next();
    };
    middleware.feed = function (action) {
        if (runningSaga && typeof runningSaga.next === 'function') runningSaga.next(action);
    };
    return middleware;
}

// Other middlewares we usually compose with sagas: a logger, a thunk shim.
const logger = ({ getState }) => (next) => (action) => {
    if (typeof console !== 'undefined') console.log('[mw]', action.type);
    return next(action);
};
const thunk = ({ dispatch, getState }) => (next) => (action) =>
    typeof action === 'function' ? action(dispatch, getState) : next(action);

// The saga we will register.
function* rootSaga(api) {
    let action;
    while (true) {
        action = yield;
        if (action && action.type === 'PING') api.dispatch({ type: 'PONG' });
    }
}

function reducer(state, action) {
    state = state || { lastSeen: null };
    return Object.assign({}, state, { lastSeen: action.type });
}

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, { lastSeen: null }, applyMiddleware(logger, thunk, sagaMiddleware));
sagaMiddleware.run(rootSaga); // <-- after createStore, never before

store.dispatch({ type: 'PING' });
console.log('after PING, lastSeen:', store.getState().lastSeen);
store.dispatch((dispatch) => dispatch({ type: 'THUNK_FIRED' }));
console.log('after thunk, lastSeen:', store.getState().lastSeen);

The order in the bootstrap is the load-bearing detail. applyMiddleware(logger, thunk, sagaMiddleware) is the conventional stack: logger sees every action first, thunk handles function-typed actions, sagaMiddleware feeds the saga runtime, and the reducer is at the end of the chain. sagaMiddleware.run(rootSaga) has to come after createStore because the run call needs the live dispatch and getState references that the middleware captured during applyMiddleware. Putting it before, or forgetting it entirely, is the bug that produces the silent failure mode where every dispatched action is logged but no saga ever fires. Mixing thunks and sagas in one app is fine; thunks for fire-and-forget side effects, sagas for anything that needs cancellation, debouncing, or coordinated multi-step workflows.