xkawi / react-universal-saga

Universal React Starter Kit ft. Redux Saga
https://react-universal-saga.herokuapp.com
MIT License
186 stars 33 forks source link

How to make sagas hot reloadable ? #8

Closed ghost closed 7 years ago

ghost commented 7 years ago

React-universal-saga is great, but sagas seem can't be hot reloaded ?

xkawi commented 7 years ago

could you try this approach? https://github.com/yelouafi/redux-saga/pull/422

to be specific, edit webpack/dev.config.js and change line 70 to something like this: 'webpack-hot-middleware/client?path=http://' + host + ':' + port + '/__webpack_hmr&reload=true'

let me know if it works. otherwise, think have to check redux-saga docs or issues to answer your question, because it is specific to redux-saga. though I would look into it myself when I have time.

xkawi commented 7 years ago

@vimniky I tested hot reloading any sagas without the possible fix I mentioned earlier, the page does reload when I made any change to sagas/index.js.

For instance, when I purposely made eslint error, the page reloaded and shows the eslint errors overlay. When I fixed the error, the page reloaded and the error overlay is gone. This happen without me touching the terminal. Terminal reported something like this when I made any changes to saga files:

webpack built 0326d6ceb9d6d7de1f78 in 3059ms
webpack building...
~ Webpack build status: OK ~
webpack built 0326d6ceb9d6d7de1f78 in 1986ms

would you elaborate further what do you mean by "can't be hot reloaded"?

ghost commented 7 years ago

@xkawi Thanks ! and sorry for my late.

I tested hot reloading any sagas without the possible fix I mentioned earlier, the page does reload when I made any change to sagas/index.js.

Yet hot reloading works great in react-universal-saga. But I have tried another way to hot reloading saga and here's the basic idea: // configStore.js

import { createStore, applyMiddleware, compose } from 'redux'
import { fromJS } from 'immutable'
import createSagaMiddleware, { END } from 'redux-saga'
import reducers from './reducers'
import SagaManager from './rootSaga'

const sagaMiddleware = createSagaMiddleware()
const devtools = (process.env.NODE_ENV === 'development' && typeof window !== 'undefined' && window.devToolsExtension) || (() => noop => noop)

// hot relaod sagas
// https://gist.github.com/hoschi/6538249ad079116840825e20c48f1690

export default function configureStore(initialState = {}) {
  // Create the store with two middlewares
  // 1. sagaMiddleware: Makes redux-sagas work
  // 2. routerMiddleware: Syncs the location/URL path to the state
  const middlewares = [
    sagaMiddleware,
  ]

  const enhancers = [
    applyMiddleware(...middlewares),
    devtools(),
  ]

  const store = createStore(
    reducers,
    fromJS(initialState),
    compose(...enhancers)
  )

  // Create hook for async sagas
  store.runSaga = sagaMiddleware.run
  store.startAbortableSaga = () => SagaManager.startSaga(sagaMiddleware)
  store.close = () => store.dispatch(END)

  if (module.hot) {
    module.hot.accept('./reducers', () => {
      const nextReducers = require('./reducers').default // eslint-disable-line global-require
      store.replaceReducer(nextReducers)
    })

    module.hot.accept('./rootSaga', () => {
      SagaManager.cancelSaga(store)
      require('./rootSaga').default.startSaga(sagaMiddleware) // eslint-disable-line global-require
    })
  }
  return store
}

// rootSaga.js

import { fork, cancel, take } from 'redux-saga/effects'
import aSaga from './path/to/aSaga'
import bSaga from './path/to/bSaga'

export function* rootSaga() {
  yield [
    fork(a),
    fork(b),
   // ....
  ]
}
export const CANCEL_SAGAS_HMR = 'CANCEL_SAGAS_HMR'

function createAbortableSaga(saga) {
  if (process.env.NODE_ENV === 'development') {
    return function* main() {
      const sagaTask = yield fork(saga)
      yield take(CANCEL_SAGAS_HMR)
      yield cancel(sagaTask)
    }
  }
  return saga
}

const SagaManager = {
  startSaga(sagaMiddleware) {
    return sagaMiddleware.run(createAbortableSaga(rootSaga))
  },
  cancelSaga(store) {
    store.dispatch({
      type: CANCEL_SAGAS_HMR,
    })
  },
}

export default SagaManager

// Here's the server-side code // I'm using react-router v4

import React from 'react'
import { renderToString } from 'react-dom/server'
import { Provider } from 'react-redux'
import { ServerRouter, createServerRenderContext } from 'react-router'
import render from './render'
import configureStore from '../shared/configStore'
import { rootSaga } from '../shared/rootSaga'
import App from '../shared/modules/App/index'

/**
 * An express middleware that is capabable of doing React server side rendering.
 */
export default function universalMiddleware(req, res) {

  // First create a context for <ServerRouter>, which will allow us to
  // query for the results of the render.
  const context = createServerRenderContext()
  const store = configureStore({})
  // Create the application react element.
  const rootComponent = (
    <Provider store={store}>
      <ServerRouter
        location={req.url}
        context={context}
      >
        <App />
      </ServerRouter>
    </Provider>
  )

  const result = context.getResult()

  // Check if the render result contains a redirect, if so we need to set
  // the specific status and redirect header and end the res.
  if (result.redirect) {
    res.status(301).setHeader('Location', result.redirect.pathname)
    res.end()
    return
  }
  try {
    store.runSaga(rootSaga).done.then(() => {
      const html = render(
        rootComponent,
        store.getState().toJS()
      )
      res.send(html)
    })

    // Trigger sagas for component to run
    // https://github.com/yelouafi/redux-saga/issues/255#issuecomment-210275959
    renderToString(rootComponent)
    // Dispatch a close event so sagas stop listening after they're resolved
    store.close()
  } catch (ex) {
    res.status(500).send(`Error during rendering: ${ex}!`)
  }
}

// Client entry:


import React from 'react'
import { render } from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import { BrowserRouter } from 'react-router'
import { Provider } from 'react-redux'
import configureStore from '../shared/configStore'
import App from '../shared/modules/App/index'
import { IS_HOT_DEVELOPMENT } from '../common/config'

const container = document.querySelector('#app')
const initialState = window.APP_STATE || {} // eslint-disable-line
const store = configureStore(initialState)
// start rootSagas on client
store.startAbortableSaga()

function renderApp(RootComponent) {
  render(
    <AppContainer>
      <BrowserRouter>
        <Provider store={store}>
          <RootComponent />
        </Provider>
      </BrowserRouter>
    </AppContainer>,
    container
  )
}

// The following is needed so that we can support hot reloading our application.
if (process.env.NODE_ENV === 'development' && module.hot && IS_HOT_DEVELOPMENT) {
  // Accept changes to this file for hot reloading.
  module.hot.accept('./index.js')
  // Any changes to our App will cause a hotload re-render.
  modul.hot.accept(
    '../shared/modules/App/index',
    () => renderApp(require('../shared/modules/App/index').default) // eslint-disable-line
  )
}
renderApp(App)