Redux saga & redux toolkit

Viewed 29883

I been trying to introduce redux sagas and redux tool kit to my project. The problem I am having at the moment is that the watcher saga is not catching on the dispatched action in the takeEvery effect and running the handler. I can't see anything wrong with the code. Can anyone Help!!!

import { configureStore, getDefaultMiddleware  } from '@reduxjs/toolkit'
import createSagaMiddleware from 'redux-saga'
import logger from 'redux-logger';

import createReducer from './rootReducer';
import sagas from './rootSaga';


const configureAdminStore = (initialState = {}) => {
    const sagaMiddleware = createSagaMiddleware();
  
    // sagaMiddleware: Makes redux-sagas work
    const middlewares = [sagaMiddleware, logger];

  
    const store = configureStore({
      reducer: createReducer(),
      middleware: [...getDefaultMiddleware({thunk: false}), ...middlewares],
      preloadedState: initialState,
      devTools: process.env.NODE_ENV !== 'production',
    });
    
    sagaMiddleware.run(sagas);
  
    return store;
  }

  export default configureAdminStore;

import {put, take, takeEvery, call} from 'redux-saga/effects'
import {getAll} from './environmentSlice'
import {confApi} from '../../service/conf-api'
import { getData } from '../../lib/conf-api-response';


function* getAllEnvironments() {
    const response = yield call(confApi.admin.getEnvironments());
    const {environments} = yield call(getData(response));
    yield put(getAll(environments));
}

// eslint-disable-next-line import/prefer-default-export
export function* watchGetAllEnvironments() {
     yield takeEvery(getAll().type, getAllEnvironments);
}

import { createSlice } from '@reduxjs/toolkit'

const environmentSlice = createSlice({
    name: 'environments',
    initialState: [],
    reducers: {
        getAll: (state, action) => {
            state = action.payload
        },
    },
  })

  export const {getAll} = environmentSlice.actions

  export const { getAllSuccess } = environmentSlice.actions;
  
  export default environmentSlice.reducer

  export const environmentSelector = (state) => state.environments

import {all} from 'redux-saga/effects'
import {watchGetAllEnvironments} from './environments/environmentSaga'

export default function* rootSaga() {
    yield all([
        watchGetAllEnvironments(),
    ])
  }

5 Answers

If you're interested in creating sagas that can resolve/reject async thunk actions then have a look at the saga-toolkit package - I created and use.

slice.js

import { createSlice } from '@reduxjs/toolkit'
import { createSagaAction  } from 'saga-toolkit'

const name = 'example'

const initialState = {
  result: null,
  loading: false,
  error: null,
}

export const fetchThings = createSagaAction(`${name}/fetchThings`)
export const doSomeMoreAsyncStuff = createSagaAction(`${name}/doSomeMoreAsyncStuff`)

const slice = createSlice({
  name,
  initialState,
  extraReducers: {
    [fetchThings.pending]: () => ({
      loading: true,
    }),
    [fetchThings.fulfilled]: ({ payload }) => ({
      result: payload,
      loading: false,
    }),
    [fetchThings.rejected]: ({ error }) => ({
      error,
      loading: false,
    }),
  },
})

export default slice.reducer

sagas.js

import { call } from 'redux-saga/effects'
import { takeLatestAsync, takeEveryAsync, putAsync } from 'saga-toolkit'
import API from 'hyper-super-api'
import * as actions from './slice'

function* fetchThings() {
  const result = yield call(() => API.get('/things'))

  const anotherResult = yield putAsync(actions.doSomeMoreAsyncStuff()) // waits for doSomeMoreAsyncStuff to finish !

  return result
}

function* doSomeMoreAsyncStuff() {
  ...
  return 'a value for another result'
}

export default [
  takeLatestAsync(actions.fetchThings.pending, fetchThings), // takeLatestAsync: behaves a bit like debounce
  takeEveryAsync(actions.doSomeMoreAsyncStuff.pending, doSomeMoreAsyncStuff), // each action will start a new saga thread
]

It looks like you are just taking getAll().type twice - once in watchGetAllEnvironments and once in getAllEnvironments.

Which means that watchGetAllEnvironments will execute getAllEnvironments, but that will immediately pause and wait for another getAll action to be dispatched, which probably never happens.

So you probably want to remove that first take in getAllEnvironments.

Also, you can just take(getAll), there's not need to take(getAll().type).

Official doc might be helpful,

import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
  const middlewares = [loggerMiddleware, thunkMiddleware]
  const middlewareEnhancer = applyMiddleware(...middlewares)

  const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
  const composedEnhancers = composeWithDevTools(...enhancers)

  const store = createStore(rootReducer, preloadedState, composedEnhancers)

  if (process.env.NODE_ENV !== 'production' && module.hot) {
    module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
  }

  return store
}

Source: official example

If this would help any new comers, I found redux-toolkit cool but still a bit complicated, especially when you throw in sagas (which is essentially async magic and mystery at first). I ended up making something similar I called saga slice. It's a spin off on redux toolkit but with sagas added to the mix from the getgo. You basically have your reducers, which manipulate state, and your sagas, which make asynchronous called and dispatch other states. To simplify the example you set, it would end up looking like this:

import { put } from "redux-saga/effects";
import { createModule } from 'saga-slice';

const environmentSlice = createModule({
    name: 'environments',
    initialState: [],
    reducers: {
        getAll: () => {},
        getAllSuccess: (state, payload) => {
            
            state.splice(0, state.length);

            payload.forEach(data => state.push(data));
        },
    },

    sagas: (A) => ({
        *[A.getAll]({ payload }) {
            
            
            const response = yield call(confApi.admin.getEnvironments());
            const { environments } = yield call(getData(response));
            

            yield put(A.getAllSuccess(environments));
        }
    })
});

This would essentially do the same thing you're trying to do without the confusing watchGetAllEnvironments and rootSaga stuff (which I find very puzzling). Saga slice is configurable to use takeEvery, takeOne, or whatever other effects you want to use without too much hassle. It's configuration objects, not yield takeEvery(). I also include helpers to facilitate some of the stuff you're doing by including an axios wrapper that ties in with saga's task cancellation, stream lines request lifecycles, and builds default CRUD modules based on standard REST principles. Check out documentation for a more thorough explanation and feel free to open issues!

Related