If you are using React Router, you might use the same component on multiple routes. For example, let's say you have an e-book app. Your React app lets people read a book. And your router is structured like this:
<Route path="books/:bookId" element={<BookInfo />} />
<Route path="books/:bookId/page/:pageId" element={<BookPage />} />
When your users move from /books/abc/page/1
to /books/abc/page/2
, the BookPage
component stays rendered. Just the props change. In this case, the pageId
prop.
And that prop change can trigger a whole bunch of things. Like fetching page 2 from the server or whatever.
Usually, there's no problem.
But sometimes you might run into issues.
Let's imagine the user can highlight important lines from the page, and add notes. Each time they add a note or highlight part of the page, you check for changes, and autosave any changes to the server.
You can useEffect
to reset state etc on a prop change, which most of the time is what I would do.
// clear state when page changes
React.useEffect(() => {
return () => {
setNotes(false); // clear notes
setHighlights([]); // clear highlights
}
}, [pageId]);
But in a few situations, when I had data stored in state, memo and refs, I found it hard to reset all of these data points at the same time. So there would be a brief moment where the pageId
would be new, but the old state would still be stored.
And if you have things like autosave triggering off comparing these different data sources, well - navigating between routes can get a little buggy.
Forcing the component to remount means you start fresh with each route change (of turn of the page in this case!).
And it's really easy to do.
Force a remount in React Router
To force a remount in React Router, you can use the key
prop. And that's not a prop that is specific to React Router - it's a special prop from ReactJS itself.
But you will mostly use the key
prop to keep lists in order in ReactJS.
So if you have a setup like this:
<Route path="photos/team" element={<Photos type="team" />} />
<Route path="photos/projects" element={<Photos type="projects" />} />
You can give your components a unique key prop, and when that prop changes, the component will remount.
<Route path="photos/team" element={<Photos key="team" type="team" />} />
<Route path="photos/projects" element={<Photos key="projects" type="projects" />} />
The key
prop is special, React uses it internally, so your component won't actually receive it. You still need to pass the type
prop to get that info into the component.
I've not found anything in the React docs specifically for this use case of the key
prop. But Kent C. Dodds shows a similar case for handling form state. And Kent knows a thing or two about React.
So, we can use the key
prop to force a remount in React Router.
Is this a hack? Possibly. Does it work? Yes.
Remount component on URL change
But what about our route parameters?
You might remember this route from the first example.
<Route path="books/:bookId/page/:pageId" element={<BookPage />} />
This won't work.
<Route path="books/:bookId/page/:pageId" element={<BookPage key=":pageId" />} />
Every page will just have the key
":pageId". And since every page has the same key
, that's no remount for you or me. Damn! I thought this was going to be easy.
This won't work either.
<Route path="books/:bookId/page/:pageId" element={<BookPage key={:pageId} />} />
No, no, capital NO.
I use a parent component to make this happen.
import { useParams } from "react-router-dom";
import BookPage from './Page';
// reloads the page component on pageId param change
function PageReload() {
let { pageId } = useParams();
return (
<BookPage key={pageId} />
);
};
export default PageReload;
And then in the Route:
<Route path="books/:bookId/page/:pageId" element={<PageReload />} />
Now whenever the pageId
changes, the PageReload
component sends the key
to the Page
component, and ✨TA-DA✨, we get a reload.
But that's pretty hard-coded. If you need this in multiple routes with different components, here's a higher-order component to make things more reusable:
import { useParams } from "react-router-dom";
// hoc to reload a component on param change
function withParamReload(WrappedComponent, paramName) {
let params = useParams();
return (
<WrappedComponent key={params[paramName]} />
);
};
export default withParamReload;
Now we can add this higher-order component to any component we need reloading on a route param change, for example, our BookPage
component:
import withParamReload from './withParamReload'
function BookPage(props) {
//...
};
export default withParamReload(BookPage, "pageId");
And the router can go back to:
<Route path="books/:bookId/page/:pageId" element={<BookPage />} />
Remount component on prop change
It might not be a URL change or react-router route that triggers your need for a remount. It might be a prop change.
But the solution is the same. Use the key
prop!
import ChildComponent from './ChildComponent'
function ParentComponent(props) {
// ...
return (
<div>
<h1>Amazing App</h1>
<ChildComponent key={id} id={id} title="Amazing" description="Super cool." />
</div>
)
};
Now ChildComponent
will remount whenever the id
prop changes.
Only re-render when you need to! Don't start using the key
prop everywhere because rerendering too much will hurt the performance of your React app. Most of the time, React will take care of rendering and you don't need to force it. Use with care!