3

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.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Luke Rhodes
  • 293
  • 3
  • 13

1 Answers1

1

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.

Long story short, you can't. React components rerender for one of three reasons:

  1. Local component state is updated.
  2. Passed prop values are updated.
  3. The parent/ancestor component updates.

The reason using the memo HOC doesn't work here though is because the Routes component only matches and renders a single Route component'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 component lifecycle is intended to work. Memoizing a component output can't do anything for when a component is being mounted/unmounted.

If what you are really trying to minimize/reduce/avoid is duplicate asynchronous calls and data fetching/refetching upon component 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 component 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.com/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>
  );
}

Edit react-router-v6-re-renders-on-route-change

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thanks for your detailed response @Drew Reese. I'll mark it as correct but what I'm really looking for is something more like react-router-cache-route for v6. Each route is loading a feed of data from GraphQL using relay, and I'd like to keep it around in the DOM so that Relay's pagination cursors don't become out of sync when navigating back/forth in the browser history (which happens when using Relay's store-and-network fetch policy). Looks like I'll need to see if Apollo works more consistently. – Luke Rhodes Sep 07 '22 at 23:47
  • @LukeRhodes I see. I've not much of any experience with Apollo/GraphQL, but based on my experience with Redux and Redux-ToolKit Query I think it's preferable to keep the "Query" part of my code caching responses de-coupled from other logical aspects of apps like routing/navigation. It's the components rendered on routes that make/handle the data requests, and the "API" code handles caching responses. Different tools for different-ish use cases. – Drew Reese Sep 07 '22 at 23:54