React Hot Reload with Redux-Saga - javascript

I'm using React Hot Reloader on my project and recently noticed a strange behavior of the redux store in pairs with Hot Reloader.
When I load the page for the first time, my page component calls an action to retrieve a particular data (this is doing in useEffect hook), everything works fine - action triggers once and calls backend properly:
When I make a change in the code and save it, Hot Reloader reloads my component and dispatch action twice (as a result - 2 calls in the network tab):
Same story with further code changes:
I wrap my main app component and redux store with hot reload middleware:
index.js
// Create a reusable render method that we can call more than once
const render = () => {
// Dynamically import our main App component, and render it
const MainApp = require('./MainApp').default;
ReactDOM.render(
<MainApp />,
rootEl,
);
};
if (module.hot) {
module.hot.accept('./MainApp', () => {
const MainApp = require('./MainApp').default;
render(
<MainApp />,
rootEl,
);
});
}
render();
store.js
const store = createStore(reducers(history), initialState,
composeEnhancers(applyMiddleware(...middlewares)));
sagaMiddleware.run(rootSaga);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers/index', () => {
const nextRootReducer = require('../reducers/index');
store.replaceReducer(nextRootReducer);
});
}
Are there any ways to fix that? Why it is happening?

Related

How can I implement a Telemetry Initializer to overwrite PageView names with the current route?

I'm implementing a ReactJS app which is sending telemetry to App Insights. It's tracking PageViews ok except every PageView has the name "<App Name>" because that's our page title and it does not change between routes (for "reasons"). This makes our App Insights PageView data not as helpful as it could be.
I want to send the current route as the PageView name instead of the page title using a telemetry initializer. Problem is I get an error when I try to use the useLocation hook to get the current route:
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
Am using react-dom v17 and react-router-dom v6, so I'm probably "breaking the Rules". I might be using the App Insights SDK incorrectly as well.
Here is my <App /> where I'm currently using App Insights:
import { AppInsightsContext, ReactPlugin, withAITracking } from '#microsoft/applicationinsights-react-js';
import { ApplicationInsights, PageView } from '#microsoft/applicationinsights-web';
//<snip>
const reactPlugin = new ReactPlugin();
const appInsights = new ApplicationInsights({
config: {
connectionString: 'myconnectionstring',
extensions: [reactPlugin],
enableAutoRouteTracking: true,
enableCorsCorrelation: true,
enableRequestHeaderTracking: true,
enableResponseHeaderTracking: true,
},
);
appInsights.addTelemetryInitializer((envelope) => {
const telemetryItem = envelope.data.baseData;
if (envelope.name === PageView.envelopeType) {
const { pathname } = useLocation(); // runtime error
telemetryItem.name = pathname;
}
});
appInsights.loadAppInsights();
const App: React.FC = () => {
return (
<Provider store={store}>
<BrowserRouter>
<AppInsightsContext.Provider value={reactPlugin}>
<AppBootstrap />
<ToastContainer />
</AppInsightsContext.Provider>
</BrowserRouter>
</Provider>
);
};
export default withAITracking(reactPlugin, App);
Any ideas how I might be able to do this? Kinda new to React!

how to listen for route change in react-router-dom v6

am trying to migrate the old react router dom code to v6 and I want to know how to listen for route change, I am now using useHistory
const history = useHistory()
//then
history.listen(...)
I did read the new docs and I did find that useHistory was changed to useNavigate
const navigate = useNavigate()
//then
navigate.listen(...) // listen is not a function
can you please help me find a way to listen to the route change in v6
// This is a React Router v6 app
import { useNavigate } from "react-router-dom";
function App() {
let navigate = useNavigate();
function handleClick() {
navigate("/home");
}
return (
<div>
<button onClick={handleClick}>go home</button>
</div>
);
}
From documentation (https://reactrouter.com/en/main/hooks/use-location), use this hook
let location = useLocation();
React.useEffect(() => {
ga('send', 'pageview');
}, [location]);
The navigate function is a function, not an object like the older react-router-dom version 5's history object.
You can still create a custom history object but you'll need to create a custom router to use it. This allows you to import your history object and create listeners.
Create a custom router example, use one of the higher-level routers as an example for how they manage the location and state, i.e. BrowserRouter:
const CustomRouter = ({ history, ...props }) => {
const [state, setState] = useState({
action: history.action,
location: history.location
});
useLayoutEffect(() => history.listen(setState), [history]);
return (
<Router
{...props}
location={state.location}
navigationType={state.action}
navigator={history}
/>
);
};
In your code create the custom history object for use by your new custom router and other components. Ensure you have history#5 installed as a project dependency. This is the same version used by RRDv6. If you need to install it run npm i history#5 to add it to the project's dependencies.
const history = createBrowserHistory();
export default history;
Use your router and pass your history object to it.
import CustomRouter from '../CustomRouter';
import history from '../myHistory';
...
<CustomRouter history={history}>
....
</CustomRouter>
In a component you want to listen to location changes on, import your history object and invoke the listen callback as you did previously.
import history from '../myHistory';
...
useEffect(() => {
const unlisten = history.listen((location, action) => {
// ... logic
});
return unlisten;
}, []);
If you want, you may be able to also create your own custom useHistory hook that simply returns your history object.
Update
react-router-dom has started exporting a HistoryRouter for a use case like this. Instead of importing the low-level Router and implementing the internal logic you import unstable_HistoryRouter as HistoryRouter and pass your custom history object (memory, hash, etc).
import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom";
import history from "../myHistory";
...
<HistoryRouter history={history}>
....
</HistoryRouter>
Notes on RRDv6.4+
If you are using RRDv6.4+ and not using the Data routers the good-ish news is that unstable_HistoryRouter is still being exported through at least RRDv6.8.0. You can follow along the filed issue in the repo here.
If you are using the Data routers then the new "unstable" method is to use an attached navigate function from the router object directly.
Example:
import { createBrowserRouter } from 'react-router-dom';
// If you need to navigate externally, instead of history.push you can do:
router.navigate('/path');
// And instead of history.replace you can do:
router.navigate('/path', { replace: true });
// And instead of history.listen you can:
router.subscribe((state) => console.log('new state', state));
I've had mixed results with using the history.listen solution between versions 6.4 and 6.8, so probably best to keep an eye on the linked issue for whatever the RRD maintainers say is the current "unstable" method of accessing the "history".
To add to the accepted answer (can't comment, not enough rep points), subscribing to the history through a useEffect with location.pathname in the dependency array won't work if the navigation unmounts the component you're attempting to call the useEffect from.
If you need to react to a change in the route due to back button specifically:
In react-router-dom v6.8.0 or even earlier, trying to attach a listener to the history, will throw an error: A history only accepts one active listener.
I learnt that react-router-dom seems to introduce a lot of changes between the minor versions as well, so you should take words like unsafe and unstable , like in unstable_HistoryRouter especially serious. They will break sooner or later, if you're not very lucky.
In my case I had to upgrade to get the reintroduced optional route params, and the UNSAFE_NavigationContext my former colleague decided to use, didn't work anymore.
So here's a high level approach, that allows you to listen to the actions on the Router's history stack, without attaching another listener to the router yourself. Which is fine, as it already has one listener by default, and it's just not exposed, but the actions derived from it are, which is enough.
In the following example we are reacting to changes in location and for each change, we check if it was due to a POP action, thats e.g. triggered when the browser's back button is used, and then execute whatever..
import { useEffect } from "react";
import {
Location,
NavigationType,
useLocation,
useNavigationType,
} from "react-router-dom";
export const useBackListener = (callback: () => void) => {
const location: Location = useLocation();
const navType: NavigationType = useNavigationType();
useEffect(() => {
if (navType === "POP" && location.key !== "default") {
if (someCondition === true) callback();
else {
doSomethingElse();
}
}
}, [location]);
};

React components not updating from Redux state after Hot Module Reload

I am using react-hot-loader 3.0.0-beta.6 to hot reload react components. Reloading itself works well - i see the updated component immediately. Unfortunately, after successful reload, dispatching actions inside the application does not trigger rerenders any more and I need to do a full manual refresh to get the application working again. The dispatched actions update the redux store, but the components are not rerendered.
All the components I am using consist of a connected container and a stateless component.
What could be the reason for not rendering the updated state? How could I continue debugging?
MyComponent/container.js:
const mapStateToProps = state => ({
...
});
const mapDispatchToProps = dispatch =>
bindActionCreators({
...
}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Component);
MyComponent/component.jsx:
const Component = ({ testProp1, testProp2 }) => (
<div onClick={testProp2}>
{testProp1}
</div>
);
Here is the successful update:
[WDS] App updated. Recompiling...
[WDS] App hot update...
[HMR] Checking for updates on the server...
[HMR] Updated modules:
[HMR] - ./source/modules/DropDown/index.js
...
[HMR] - ./source/modules/App/index.js
Render in the main.jsx:
const render = () => {
ReactDOM.render(
<AppContainer>
<Provider store={store}>
<MuiThemeProvider>
<App />
</MuiThemeProvider>
</Provider>
</AppContainer>,
eyeTalLayer
);
};
render();
if (module.hot) {
module.hot.accept('./modules/App', render);
}
There's actually a couple open issues in the React-Redux repo discussing similar behavior as of React-Redux 5.0. See react-redux#636 and react-redux#670.
I did a bit of research, and it looks like the components higher in the hierarchy are getting recompiled and hot-reloaded, but not the components lower in the hierarchy. Because v5 implemented a top-down subscription system, the lower components aren't aware that their subscription references are now stale. I haven't had time yet to try to figure out a good way to handle that.
Based on what I've seen, I believe that removing the use of React-Hot-Loader will work around the problem. You could just reload the entire component tree using "plain" HMR, and I see you've already got your code set up to do that. You'd lose the potential benefits of RHL trying to maintain component state, but the Redux connections should reset properly.

react router redux initial state setting on isomorphic app

I am writing an isomorphic app with react, redux, react-router, react-router-redux.
I am calling syncHistoryWithStore on client.js. But on initial load, router.locationBeforeTransitions is always null. It is populated once I navigate though.
const history = syncHistoryWithStore(browserHistory, store);
...
<Router routes={routes} history={history} />
Above code is from client.js
Should I fire LOCATION_CHANGE action on server side manually to populate initial state of react-router-redux?
You will be creating a new store on client hence the state from the store created on server is lost. We will need to pass the initial state from server to the client, so it can populate its store.
on Server:
const initialState = store.getState();
now add this to your HTML template rendered from server:
<script type="application/javascript">
window.__INITIAL_STATE__ = ${JSON.stringify(initialState)};
</script>
on Client:
let initialState = window.__INITIAL_STATE__;
use the initialState when you createStore on Client.

How to dynamically load reducers for code splitting in a Redux application?

I'm going migrate to Redux.
My application consists of a lot of parts (pages, components) so I want to create many reducers. Redux examples show that I should use combineReducers() to generate one reducer.
Also as I understand Redux application should have one store and it is created once the application starts. When the store is being created I should pass my combined reducer. This makes sense if the application is not too big.
But what if I build more than one JavaScript bundle? For example, each page of application has own bundle. I think in this case the one combined reducer is not good. I looked through the sources of Redux and I have found replaceReducer() function. It seems to be what I want.
I could create combined reducer for each part my application and use replaceReducer() when I move between parts of application.
Is this a good approach?
Update: see also how Twitter does it.
This is not a full answer but should help you get started. Note that I'm not throwing away old reducers—I'm just adding new ones to the combination list. I see no reason to throw away the old reducers—even in the largest app you're unlikely to have thousands of dynamic modules, which is the point where you might want to disconnect some reducers in your application.
reducers.js
import { combineReducers } from 'redux';
import users from './reducers/users';
import posts from './reducers/posts';
export default function createReducer(asyncReducers) {
return combineReducers({
users,
posts,
...asyncReducers
});
}
store.js
import { createStore } from 'redux';
import createReducer from './reducers';
export default function configureStore(initialState) {
const store = createStore(createReducer(), initialState);
store.asyncReducers = {};
return store;
}
export function injectAsyncReducer(store, name, asyncReducer) {
store.asyncReducers[name] = asyncReducer;
store.replaceReducer(createReducer(store.asyncReducers));
}
routes.js
import { injectAsyncReducer } from './store';
// Assuming React Router here but the principle is the same
// regardless of the library: make sure store is available
// when you want to require.ensure() your reducer so you can call
// injectAsyncReducer(store, name, reducer).
function createRoutes(store) {
// ...
const CommentsRoute = {
// ...
getComponents(location, callback) {
require.ensure([
'./pages/Comments',
'./reducers/comments'
], function (require) {
const Comments = require('./pages/Comments').default;
const commentsReducer = require('./reducers/comments').default;
injectAsyncReducer(store, 'comments', commentsReducer);
callback(null, Comments);
})
}
};
// ...
}
There may be neater way of expressing this—I'm just showing the idea.
This is how I implemented it in a current app (based on code by Dan from a GitHub issue!)
// Based on https://github.com/rackt/redux/issues/37#issue-85098222
class ReducerRegistry {
constructor(initialReducers = {}) {
this._reducers = {...initialReducers}
this._emitChange = null
}
register(newReducers) {
this._reducers = {...this._reducers, ...newReducers}
if (this._emitChange != null) {
this._emitChange(this.getReducers())
}
}
getReducers() {
return {...this._reducers}
}
setChangeListener(listener) {
if (this._emitChange != null) {
throw new Error('Can only set the listener for a ReducerRegistry once.')
}
this._emitChange = listener
}
}
Create a registry instance when bootstrapping your app, passing in reducers which will be included in the entry bundle:
// coreReducers is a {name: function} Object
var coreReducers = require('./reducers/core')
var reducerRegistry = new ReducerRegistry(coreReducers)
Then when configuring the store and routes, use a function which you can give the reducer registry to:
var routes = createRoutes(reducerRegistry)
var store = createStore(reducerRegistry)
Where these functions look something like:
function createRoutes(reducerRegistry) {
return <Route path="/" component={App}>
<Route path="core" component={Core}/>
<Route path="async" getComponent={(location, cb) => {
require.ensure([], require => {
reducerRegistry.register({async: require('./reducers/async')})
cb(null, require('./screens/Async'))
})
}}/>
</Route>
}
function createStore(reducerRegistry) {
var rootReducer = createReducer(reducerRegistry.getReducers())
var store = createStore(rootReducer)
reducerRegistry.setChangeListener((reducers) => {
store.replaceReducer(createReducer(reducers))
})
return store
}
Here's a basic live example which was created with this setup, and its source:
Example
Source
It also covers the necessary configuration to enable hot reloading for all your reducers.
There is now a module that adds injecting reducers into the redux store. It is called Redux Injector.
Here is how to use it:
Do not combine reducers. Instead put them in a (nested) object of functions as you would normally but without combining them.
Use createInjectStore from redux-injector instead of createStore from redux.
Inject new reducers with injectReducer.
Here is an example:
import { createInjectStore, injectReducer } from 'redux-injector';
const reducersObject = {
router: routerReducerFunction,
data: {
user: userReducerFunction,
auth: {
loggedIn: loggedInReducerFunction,
loggedOut: loggedOutReducerFunction
},
info: infoReducerFunction
}
};
const initialState = {};
let store = createInjectStore(
reducersObject,
initialState
);
// Now you can inject reducers anywhere in the tree.
injectReducer('data.form', formReducerFunction);
Full Disclosure: I am the creator of the module.
As of October 2017:
Reedux
implements what Dan suggested and nothing more, without touching your store, your project or your habits
There are other libraries too but they might have too many dependencies, less examples, complicated usage, are incompatible with some middlewares or require you to rewrite your state management. Copied from Reedux's intro page:
redux-modules
redux-module-builder
redux-stack
paradux
redux-dynamic-reducer
redux-injector
redux-dynamix
We released a new library that helps modulating a Redux app and allows dynamically adding/removing Reducers and middlewares.
Please take a look at
https://github.com/Microsoft/redux-dynamic-modules
Modules provide the following benefits:
Modules can be easily re-used across the application, or between multiple similar applications.
Components declare the modules needed by them and redux-dynamic-modules ensures that the module is loaded for the component.
Modules can be added/removed from the store dynamically, ex. when a component mounts or when a user performs an action
Features
Group together reducers, middleware, and state into a single, re-usable module.
Add and remove modules from a Redux store at any time.
Use the included component to automatically add a module when a component is rendered
Extensions provide integration with popular libraries, including redux-saga and redux-observable
Example Scenarios
You don't want to load the code for all your reducers up front. Define a module for some reducers and use DynamicModuleLoader and a library like react-loadable to download and add your module at runtime.
You have some common reducers/middleware that need to be re-used in different areas of your application. Define a module and easily include it in those areas.
You have a mono-repo that contains multiple applications which share similar state. Create a package containing some modules and re-use them across your applications
Here is another example with code splitting and redux stores, pretty simple & elegant in my opinion. I think it may be quite useful for those who are looking for a working solution.
This store is a bit simplified it doesn't force you to have a namespace (reducer.name) in your state object, of course there may be a collision with names but you can control this by creating a naming convention for your reducers and it should be fine.
Following is the approach that I have followed to implement this.
We have our store file where we will have static reducers who will always be present in the reducer, and dynamic reducers will be added when required component is mounted.
reducer file
The staticReducers that will always be present in the application
const staticReducers = combineReducers({
entities1: entities1,
});
const createReducer = (asyncReducers) => {
return combineReducers({
staticReducers,
...asyncReducers,
});
};
export default createReducer;
store file
Here we can have our custom middleware, loggers etc, those we can pass in the middlewares array.and use it as follows.
import { createStore, applyMiddleware, compose } from "redux";
import createReducer from "./reducers";
import api from "./middlewares/api";
const middlewares = [ api, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer]
const composedEnhancers = composeWithDevTools(compose(...enhancers))
const store = createStore(createReducer(), composedEnhancers)
export default function configureStore() {
// Add a dictionary to keep track of the registered async reducers
store.asyncReducers = {};
// Create an inject reducer function
// This function adds the async reducer, and creates a new combined
// reducer
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer;
store.replaceReducer(createReducer(store.asyncReducers));
};
// Return the modified store
return store;
}
export function getStore() {
return store;
}
Now suppose we have a component that we want to load dynamically and that component might have its own slice(reducer), then we can call inject reducer to dynamically add its to the existing reducer.
const Counter2 = React.lazy(() =>
import("../counter2/counter2").then(async (module) => {
const entities2 = await
import("../../../store/entities2").then((todosModule) =>
todosModule.default);
store.injectReducer("entities2", entities2);
return module;
})
)
<React.Suspense fallback={<div>loading...</div>}>
<Counter2 />
</React.Suspense>
After mounting this component we will find entities2 injected to our store.

Categories

Resources