Redux Saga hot reloading

Update for redux-saga package in version 1.0.0 and newer:

Use @mpolci solution and just change

sagaTask.done.then(() => {

to

sagaTask.toPromise().then(() => {

and everything start working same.

See Task documentation


I'm working on a project with redux and redux-saga (but not react). I implemented the hot reloading of sagas using the sagaMiddleware.run() but you have to handle the module reloading and replace reducers and sagas as indicated in the link you provided (https://github.com/reactjs/react-redux/releases/tag/v2.0.0).

import { createStore } from 'redux';
import rootReducer from '../reducers/index';
import getSagas from '../sagas';

export default function configureStore(initialState) {
  const sagaMiddleware = createSagaMiddleware()
  const store = createStore(rootReducer, initialState, applyMiddleware(sagaMiddleware));
  let sagaTask = sagaMiddleware.run(function* () {
     yield getSagas()
  })
  if (module.hot) {
    // Enable Webpack hot module replacement for reducers
    module.hot.accept('../reducers', () => {
      const nextRootReducer = require('../reducers/index');
      store.replaceReducer(nextRootReducer);
    });
    module.hot.accept('../sagas', () => {
      const getNewSagas = require('../sagas');
      sagaTask.cancel()
      sagaTask.done.then(() => {
        sagaTask = sagaMiddleware.run(function* replacedSaga (action) {
          yield getNewSagas()
        })
      })
    })
  }

  return store;
}

An important thing to notice is the getSagas() function. It returns an array of freshly created generator object of sagas, you cannot have some precreated object in the array from some already running sagas. If you buid this array only in one module, you can use directly a constant array, but if you build it composing sagas from different modules you have to be sure to recreate sagas from all modules, so the better way is that all modules export creating function instead of exporting a fixed saga or array of sagas. For example it could be a function like this:

export default () => [
  takeEvery(SOME_ACTION, someActionSaga),
  takeEvery(OTHER_ACTION, otherActionSaga),
]

Obviously all sagas are restarted from beginning and if you have a complex sagas with internal state you lose the current state.

A very similar approach is to use a dynamic saga in place of calling sagaMidleware.run(), it a very similar solution but you can reload subsets of the sagas and handle them in different ways. For more info see https://gist.github.com/mpolci/f44635dc761955730f8479b271151cf2