React router 6 never unmount component when URL parameter changes - reactjs

We have a problem with the react router v6. When the URL parameters change, it is already using the mount component. Never unmount and mount the component.
Produced code example - if switching between link 1 and link 2(url parameter (id) changes), the Link component never unmount and mount:
https://stackblitz.com/edit/github-agqlf5-gohmbu?file=src/routes/link.jsx
So how do we get it to be unmount and mount component when the url parameter changes?

This is by-design. If you look at the Route rendering the Invoices component
<Route path="link/:id" element={<Invoices />} />
There is nothing here to cause (a) the Route to remount, and (b) the routed component Invoices to remount. If the route path updates it will only trigger a rerender of the routed component, only values in the routing context have changed really. Think of this more like an update to props triggering a component to rerender, not remount.
If the routed component needs to "listen" to and respond to route param changes then it should use a useEffect hook to issue any side-effects.
Example:
import { Outlet, useParams } from 'react-router-dom';
export default function Invoices() {
const { id } = useParams();
React.useEffect(() => {
console.log('mount');
return () => console.log('unmount');
}, []);
React.useEffect(() => {
console.log("Route parameter changed", { id });
}, [id]);
return (
<div style={{ display: 'flex', paddingTop: '1rem' }}>
<nav>Invoice ID: {id}</nav>
<Outlet />
</div>
);
}
If you really want to remount the routed component then create a wrapper component that reads the id route path param and sets a React key on the routed component. The InvoicesWrapper will remain mounted but when the id route param updates the Invoices component will remount since it has a new React key.
Example:
const InvoicesWrapper = () => {
const { id } = useParams();
return <Invoices key={id} />
};
...
...
<Route path="link/:id" element={<InvoicesWrapper />} />
...

Related

React Does Not Re-Render after useEffect and useState

React won't rerender after useEffect axios response updates state with useState. The query works, and if I click to another endpoint and back again, it does render successfully, but it doesn't successfully render on mount.
This is an expensive query so I just want to run it once when the website is loaded or refreshed.
const App: React.FC = () => {
const [cards, setCards] = useState<Card[]>([]);
// query server for list of cards on mount
useEffect(() => {
axios.get("http://localhost:3001/cards")
.then(response => setCards(response.data))
.catch(error => alert(`Error fetching cards. ${error}`))
}, []);
return (
<Routes>
<Route path='/' element={<LandingPage cards={cards} />} />
{/* This does not show all cards! */}
{cards.map((card, idx) => <Route key={idx} path={'/' + card.endpoint} element={<Card card={card}/>} />)}
</Routes>
)
}
First things first, you should add a key to each element of your map function to render the Route components correctly. This will also help to force a re-render of that component whenever the keychanges. Let's try that first and see what happens
Also, the console.log(cards) after your setCards(response.data) won't print because the state update operation hasn't been finished yet

React call from class to functional component. Error: Invalid hook call. Hooks can only be called inside of the body of a function component

I am calling a functional component from a class component.
This is my functional component,
import React, { useState, useEffect } from "react";
import {useNavigate} from "react-router";
const UtilsExt = () => {
const navigate = useNavigate();
navigate('/');
};
export default UtilsExt;
I am calling above from a button click event from a class component. When i do that i get this error.
Error: 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
React hooks can only be called in function components or custom React hooks. In order to use the navigate function you'll need to either create a wrapper component or a custom withNavigate HOC to pass navigate as a prop to your class component.
Wrapper
const MyComponentWithNavigation = props => {
const navigate = useNavigate();
return <MyClassComponent {...props} navigate={navigate} />;
};
Usage:
<Route path="..." element={<MyComponentWithNavigation />} />
HOC
const withNavigate = Component => props => {
const navigate = useNavigate();
return <Component {...props} navigate={navigate} />;
};
Usage:
const MyComponentWithNavigation = withNavigation(MyClassComponent);
...
<Route path="..." element={<MyComponentWithNavigation />} />
Accessing the navigate function
In your class component, access navigate from props.
this.props.navigate("/");

Accessing a URL parameter outside the element in react-router-v6

Issue: I need to access a URL parameter outside the component in v6.
I know I can use the useParams() hook inside the component, But what if I needed to access a URL parameter outside the component?
This is how I used to do it in version 5:
// v5:
<Route
path = "/blog/:id"
render = {
({match:{params}}) => <Post item={ getPostById(params.id) } />
}
/>
I am accessing the :id parameter outside the < Post /> component.
How can I write the previous code in v6?
With ({match:{params}}) => <Post item={ getPostById(params.id) } /> you've effectively written an anonymous React component that consumes a match prop and renders your Post component.
If you've components that can't use the useParams hook then write a wrapper function component that reads the id param from the useParams hook and passes the id along.
const PostWithIdParam = () => {
const { id } = useParams();
return <Post item={getPostById(id)} />;
};
...
<Route
path="/blog/:id"
element={<PostWithIdParam />}
/>

React prevent remounting components passed from props

When using React with React Router I run in some mounting issues.
This might not even be a problem with React Router itself.
I want to pass some additional data along with the child routes.
This seems to be working, however the changes on the main page trigger grandchildren to be remounted every time the state is changed.
Why is this and why doe this only happen to grandchildren an not just the children ?
Code example:
import React, { useEffect, useState } from 'react';
import { Route, Switch, BrowserRouter as Router, Redirect } from 'react-router-dom';
const MainPage = ({ ChildRoutes }) => {
const [foo, setFoo] = useState(0);
const [data, setData] = useState(0);
const incrementFoo = () => setFoo(prev => prev + 1);
useEffect(() =>{
console.log("mount main")
},[]);
useEffect(() =>{
setData(foo * 2)
},[foo]);
return (
<div>
<h1>Main Page</h1>
<p>data: {data}</p>
<button onClick={incrementFoo}>Increment foo {foo}</button>
<ChildRoutes foo={foo} />
</div>
);
};
const SecondPage = ({ ChildRoutes, foo }) => {
const [bar, setBar] = useState(0);
const incrementBar = () => setBar(prev => prev + 1);
useEffect(() =>{
console.log("mount second")
},[]);
return (
<div>
<h2>Second Page</h2>
<button onClick={incrementBar}>Increment bar</button>
<ChildRoutes foo={foo} bar={bar} />
</div>
);
};
const ThirdPage = ({ foo, bar }) => {
useEffect(() =>{
console.log("mount third")
},[]);
return (
<div>
<h3>Third Page</h3>
<p>foo: {foo}</p>
<p>bar: {bar}</p>
</div>
);
};
const routingConfig = [{
path: '/main',
component: MainPage,
routes: [
{
path: '/main/second',
component: SecondPage,
routes: [
{
path: '/main/second/third',
component: ThirdPage
},
]
}
]
}];
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
</Route>
);
})}
</Switch>
);
};
export const App = () => {
return(
<Router>
<Routing routes={routingConfig}/>
<Route exact path="/">
<Redirect to="/main/second/third" />
</Route>
</Router>
)
};
export default App;
Every individual state change in the MainPage causes ThirdPage to be remounted.
I couldn't create a snippet with StackOverflow because of the React Router. So here is a codesandbox with the exact same code: https://codesandbox.io/s/summer-mountain-unpvr?file=/src/App.js
Expected behavior is for every page to only trigger the mounting once.
I know I can probably fix this by using Redux or React.Context, but for now I would like to know what causes this behavior and if it can be avoided.
==========================
Update:
With React.Context it is working, but I am wondering if this can be done without it?
Working piece:
const ChildRouteContext = React.createContext();
const ChildRoutesWrapper = props => {
return (
<ChildRouteContext.Consumer>
{ routes => <Routing routes={routes} {...props} /> }
</ChildRouteContext.Consumer>
);
}
const Routing = ({ routes: passedRoutes, ...rest }) => {
if (!passedRoutes) return null;
return (
<Switch>
{passedRoutes.map(({ routes, component: Component, ...route }) => {
return (
<Route key={route.path} {...route}>
<ChildRouteContext.Provider value={routes}>
<Component {...rest} ChildRoutes={ChildRoutesWrapper}/>
</ChildRouteContext.Provider>
</Route>
);
})}
</Switch>
);
};
To understand this issue, I think you might need to know the difference between a React component and a React element and how React reconciliation works.
React component is either a class-based or functional component. You could think of it as a function that will accept some props and
eventually return a React element. And you should create a React component only once.
React element on the other hand is an object describing a component instance or DOM node and its desired properties. JSX provide
the syntax for creating a React element by its React component:
<Component someProps={...} />
At a single point of time, your React app is a tree of React elements. This tree is eventually converted to the actual DOM nodes which is displayed to our screen.
Everytime a state changes, React will build another whole new tree. After that, React need to figure a way to efficiently update DOM nodes based on the difference between the new tree and the last tree. This proccess is called Reconciliation. The diffing algorithm for this process is when comparing two root elements, if those two are:
Elements Of Different Types: React will tear down the old tree and build the new tree from scratch // this means re-mount that element (unmount and mount again).
DOM Elements Of The Same Type: React keeps the same underlying DOM node, and only updates the changed attributes.
Component Elements Of The Same Type: React updates the props of the underlying component instance to match the new element // this means keep the instance (React element) and update the props
That's a brief of the theory, let's get into pratice.
I'll make an analogy: React component is a factory and React element is a product of a particular factory. Factory should be created once.
This line of code, ChildRoutes is a factory and you are creating a new factory everytime the parent of the Component re-renders (due to how Javascript function created):
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
Based on the routingConfig, the MainPage created a factory to create the SecondPage. The SecondPage created a factory to create the ThirdPage. In the MainPage, when there's a state update (ex: foo got incremented):
The MainPage re-renders. It use its SecondPage factory to create a SecondPage product. Since its factory didn't change, the created SecondPage product is later diffed based on "Component Elements Of The Same Type" rule.
The SecondPage re-renders (due to foo props changes). Its ThirdPage factory is created again. So the newly created ThirdPage product is different than the previous ThirdPage product and is later diffed based on "Elements Of Different Types". That is what causing the ThirdPage element to be re-mounted.
To fix this issue, I'm using render props as a way to use the "created-once" factory so that its created products is later diffed by "Component Elements Of The Same Type" rule.
<Component
{...rest}
renderChildRoutes={(props) => (<Routing routes={routes} {...props} />)}
/>
Here's the working demo: https://codesandbox.io/s/sad-microservice-k5ny0
Reference:
React Components, Elements, and Instances
Reconciliation
Render Props
The culprit is this line:
<Component {...rest} ChildRoutes={props => <Routing routes={routes} {...props}/>}/>
More specifically, the ChildRoutes prop. On each render, you are feeding it a brand new functional component, because given:
let a = props => <Routing routes={routes} {...props}/>
let b = props => <Routing routes={routes} {...props}/>
a === b would always end up false, as it's 2 distinct function objects. Since you are giving it a new function object (a new functional component) on every render, it has no choice but to remount the component subtree from this Node, because it's a new component every time.
The solution is to create this functional component once, in advance, outside your render method, like so:
const ChildRoutesWrapper = props => <Routing routes={routes} {...props} />
... and then pass this single functional component:
<Component {...rest} ChildRoutes={ChildRoutesWrapper} />
Your components are remounting every time because you're using the component prop.
Quoting from the docs:
When you use component (instead of render or children, below) the router uses React.createElement to create a new React element from the given component. That means if you provide an inline function to the component prop, you would create a new component every render. This results in the existing component unmounting and the new component mounting instead of just updating the existing component. When using an inline function for inline rendering, use the render or the children prop (below).
The solution you probably need in your case is to edit your Routing component to use render instead of children.

what differences between Link in react-router and `window.history.pushState()`?

in my code when i click button, component Aaaaa is not re-rendered, but when i tap on link, component Aaaaa is re-rendered. i can't find cause of it?
function App() {
return (
<>
<button onClick={() => window.history.pushState('','','/about')}>About</button>
<Link to='/about'>to About</Link>
<Aaaaaa/>
</>
);
}
and:
Aaaaaa(){
const location = useLocation()
return <div>About </div>
}
The proper way is to use <Link to='/about'>to About</Link> when trying to navigate manually (by clicking a button) and window.history.pushState('','','/about') when trying to navigate automatically (like after completing an API call).
cause window.history.pushState is not using the react router
so you can use link to navigate between the pages.
but if you have limits and you want it to be a buttn and still navigate using react router, you can use history from react-router-dom
import { withRouter } from 'react-router-dom'
// some other codes
const { history } = props;
// some other codes
<button onClick={() => history.push('/about')}>About</button>
// some other codes
export default withRouter(MyComponent)
or you can use 'useHistory' hook if you're using react-router v5.
import { useHistory } from 'react-router-dom'
// some other codes
const history = useHistory();
// some other codes
<button onClick={() => history.push('/about')}>About</button>
// some other codes
export default MyComponent
I found that window.history.pushState('','','/about') does not work as expected. The router does not update what route to display.
If you cant use a button and need to control the location programatically, but use class components wrap it in a function component like this:
... other routes
<Route exact path="/register" component={()=>{
const history = useHistory();
return <RegisterPage onRegister={async (account) => {
this.setState({ account });
history.push('/'); // sends user automatically to main page
}} />
}} />
...
window.history.pushState method doesn't trigger any browser event, which in turn doesn't notify react router about the update you made to the history object
React router custom Link component is most likely going to use a custom event or the observer design pattern to be able to notify and respond to changes in the window history object

Resources