When I navigate back and forth between routes, React Router re-renders memoized routes causing useEffect(() => [])
to re-run and data to re-fetch. I'd like to prevent that and instead keep existing routes around but hidden in the dom. I'm struggling with "how" though.
The following is sample code for the problem:
import React, { useEffect } from "react";
import { BrowserRouter as Router, Route, Routes, useNavigate } from "react-router-dom";
export default function App() {
return (
<Router>
<Routes>
<Route path={"/"} element={<MemoizedRouteA />} />
<Route path={"/b"} element={<MemoizedRouteB />} />
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router A");
}, []);
return (
<button onClick={() => { navigate('/b') }}>Go to B</button>
);
};
const MemoizedRouteA = React.memo(RouteA)
function RouteB() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router B");
}, []);
return (
<button onClick={() => { navigate('/') }}>Go to A</button>
);
}
const MemoizedRouteB = React.memo(RouteB)
Sandbox: =/src/App.js
With the above code, you'll see that the "alert" code is called whenever you tap a button or use the browser back button.
With there being so many changes of React Router over the years I'm struggling to find a solution for this.
When I navigate back and forth between routes, React Router re-renders memoized routes causing useEffect(() => [])
to re-run and data to re-fetch. I'd like to prevent that and instead keep existing routes around but hidden in the dom. I'm struggling with "how" though.
The following is sample code for the problem:
import React, { useEffect } from "react";
import { BrowserRouter as Router, Route, Routes, useNavigate } from "react-router-dom";
export default function App() {
return (
<Router>
<Routes>
<Route path={"/"} element={<MemoizedRouteA />} />
<Route path={"/b"} element={<MemoizedRouteB />} />
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router A");
}, []);
return (
<button onClick={() => { navigate('/b') }}>Go to B</button>
);
};
const MemoizedRouteA = React.memo(RouteA)
function RouteB() {
const navigate = useNavigate()
useEffect(() => {
alert("Render Router B");
}, []);
return (
<button onClick={() => { navigate('/') }}>Go to A</button>
);
}
const MemoizedRouteB = React.memo(RouteB)
Sandbox: https://codesandbox.io/s/wonderful-hertz-w9qoip?file=/src/App.js
With the above code, you'll see that the "alert" code is called whenever you tap a button or use the browser back button.
With there being so many changes of React Router over the years I'm struggling to find a solution for this.
Share Improve this question edited Sep 7, 2022 at 4:36 Drew Reese 204k18 gold badges245 silver badges273 bronze badges asked Sep 7, 2022 at 1:16 Luke RhodesLuke Rhodes 3233 silver badges13 bronze badges 1- learn to use react-query package, it'll handle all the memoization for you. We can handle and scale the big application using it. – bhargava.prabu Commented Sep 7, 2022 at 4:38
1 Answer
Reset to default 4When I navigate back and forth between routes, React Router re-renders memoized routes causing
useEffect(() => [])
to re-run and data to re-fetch. I'd like to prevent that and instead keep existing routes around but hidden in the dom. I'm struggling with "how" though.
Long story short, you can't. React ponents rerender for one of three reasons:
- Local ponent state is updated.
- Passed prop values are updated.
- The parent/ancestor ponent updates.
The reason using the memo
HOC doesn't work here though is because the Routes
ponent only matches and renders a single Route
ponent's element
prop at-a-time. Navigating from "/"
to "/b"
necessarily unmounts MemoizedRouteA
and mounts MemoizedRouteB
, and vice versa when navigating in reverse. This is exactly how RRD is intended to work. This is how the React ponent lifecycle is intended to work. Memoizing a ponent output can't do anything for when a ponent is being mounted/unmounted.
If what you are really trying to minimize/reduce/avoid is duplicate asynchronous calls and data fetching/refetching upon ponent mounting then what I'd suggest here is to apply the Lifting State Up pattern and move the state and useEffect
call into a parent/ancestor.
Here's a trivial example using an Outlet
ponent and its provided context, but the state could be provided by any other means such as a regular React context or Redux.
import React, { useEffect, useState } from "react";
import {
BrowserRouter as Router,
Route,
Routes,
Outlet,
useNavigate,
useOutletContext
} from "react-router-dom";
export default function App() {
const [users, setUsers] = useState(0);
useEffect(() => {
fetch("https://jsonplaceholder.typicode./users")
.then((response) => response.json())
.then(setUsers);
}, []);
return (
<Router>
<Routes>
<Route element={<Outlet context={{ users }} />}>
<Route path={"/"} element={<RouteA />} />
<Route path={"/b"} element={<RouteB />} />
</Route>
</Routes>
</Router>
);
}
function RouteA() {
const navigate = useNavigate();
return (
<div>
<button onClick={() => navigate("/b")}>Go to B</button>
</div>
);
}
function RouteB() {
const navigate = useNavigate();
const { users } = useOutletContext();
return (
<div>
<button onClick={() => navigate("/")}>Go to A</button>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} : {user.email}
</li>
))}
</ul>
</div>
);
}