Skip to content

Instantly share code, notes, and snippets.

@markerikson
Last active August 1, 2022 07:41
Star You must be signed in to star a gist
Save markerikson/dc6cee36b5b6f8d718f2e24a249e0491 to your computer and use it in GitHub Desktop.
Webpack React/Redux Hot Module Reloading (HMR) example
import React from "react";
import ReactDOM from "react-dom";
import configureStore from "./store/configureStore";
const store = configureStore();
const rootEl = document.getElementById("root");
let render = () => {
const RootAppComponent = require("containers/RootAppComponent").default;
ReactDOM.render(
<RootAppComponent store={store} />,
rootEl
);
};
if(module.hot) {
// Support hot reloading of components
// and display an overlay for runtime errors
const renderApp = render;
const renderError = (error) => {
const RedBox = require("redbox-react");
ReactDOM.render(
<RedBox error={error} />,
rootEl,
);
};
render = () => {
try {
renderApp();
}
catch(error) {
renderError(error);
}
};
module.hot.accept("./containers/Root", () => {
setTimeout(render);
});
}
render();
import {createStore, applyMiddleware, compose} from "redux";
import rootReducer from "../reducers/rootReducer";
import thunk from "redux-thunk";
import createSagaMiddleware from 'redux-saga';
import SagaManager from "sagas/SagaManager";
/**
* Based on the current environment variable, we need to make sure
* to exclude any DevTools-related code from the production builds.
* The code is envify'd - using 'DefinePlugin' in Webpack.
*/
const sagaMiddleware = createSagaMiddleware();
const middlewares = [thunk, sagaMiddleware];
const storeEnhancers = [];
if(__DEV__) {
const DevTools = require("../containers/DevTools").default;
// If the user has the "Redux DevTools" browser extension installed, use that.
// Otherwise, hook up the in-page DevTools UI component.
const debugEnhancer = window.devToolsExtension ? window.devToolsExtension() : DevTools.instrument();
storeEnhancers.push(debugEnhancer);
}
const middlewareEnhancer = applyMiddleware(...middlewares);
storeEnhancers.unshift(middlewareEnhancer);
export default function configureStore(initialState) {
const store = createStore(
rootReducer,
initialState,
compose(...storeEnhancers)
);
// run sagas
SagaManager.startSagas(sagaMiddleware);
if(__DEV__) {
// Hot reload reducers (requires Webpack or Browserify HMR to be enabled)
if(module.hot) {
module.hot.accept("../reducers/rootReducer", () =>
store.replaceReducer(require("../reducers/rootReducer").default)
);
module.hot.accept('../sagas/SagaManager', () => {
SagaManager.cancelSagas(store);
require('../sagas/SagaManager').default.startSagas(sagaMiddleware);
});
}
}
return store;
}
// Borrowed from https://gist.github.com/hoschi/6538249ad079116840825e20c48f1690
// Note that reloading sagas has several issues/caveats to be aware of.
// See https://github.com/yelouafi/redux-saga/issues/22#issuecomment-218737951 for discussion.
import { take, fork, cancel } from 'redux-saga/effects';
import rootSaga from "./rootSaga";
const sagas = [rootSaga];
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);
};
} else {
return saga;
}
}
const SagaManager = {
startSagas(sagaMiddleware) {
sagas.map(createAbortableSaga).forEach((saga) => sagaMiddleware.run(saga));
},
cancelSagas(store) {
store.dispatch({
type: CANCEL_SAGAS_HMR
});
}
};
export default SagaManager;
@TSMMark
Copy link

TSMMark commented Apr 12, 2018

Thanks for this! Helped me a lot.

I'd like to add it seems like it would be good practice to ReactDOM.unmountComponentAtNode(rootEl) before ReactDOM.render

@ssaric
Copy link

ssaric commented Jan 25, 2019

You are doing lords work

@jyu-trux
Copy link

omg this is gold

@arleighdickerson
Copy link

tight

@bitflower
Copy link

New to HMR sagas: Is the still the 2019/2020 way to go? Just askin' :-)

@markerikson
Copy link
Author

If you're using sagas, yes, this HMR approach should still work as far as I know.

That said, I would say that 95% of Redux apps don't actually need sagas:

https://redux.js.org/faq/actions#what-async-middleware-should-i-use-how-do-you-decide-between-thunks-sagas-observables-or-something-else

And we would recommend starting with thunks by default:

https://redux.js.org/style-guide/style-guide#use-thunks-for-async-logic

@bitflower
Copy link

Already using sagas to „listen“ to several actions (also dynamically picked by the user like in a hook system). So I don‘t doubt our decision there.

In one of the links rxjs is mentioned. I wonder how you would achieve a similiar behavior. Gotta dig a little deeper there.

Your links are awesome resources! Thanks a bunch!

When you say „we recommend“ who do you mean?

@markerikson
Copy link
Author

"We" as the Redux team (ie, mostly me, and also Tim Dorr).

@charlie0077
Copy link

charlie0077 commented Apr 16, 2020

this still works. thanks.

The only problem i noticed is that: if you change some file(configuration for example) that is used by both your component and saga, the app may hang. The reason: component gets rerendered first, triggered some action, which triggered some saga, but then that saga gets canceled/replaced, while the component may still wait for results from old sagas

=> I just noticed that you have something like this in your example:
module.hot.accept("./containers/Root", () => {
setTimeout(render);
});

That works.
Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment