React Router V6在路由改变时重新渲染

3
当我在不同的路由之间切换时,React Router会重新渲染记忆化的路由,导致useEffect(() => [])重新运行并重新获取数据。我希望防止这种情况发生,而是将现有的路由保留在DOM中但隐藏起来。然而,我正在为“如何”解决问题而苦苦挣扎。
以下是该问题的示例代码:
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)

沙盒:https://codesandbox.io/s/wonderful-hertz-w9qoip?file=/src/App.js

通过上面的代码,您会发现每当您点击按钮或使用浏览器后退按钮时,“alert”代码都会被调用。

由于React Router多年来发生了许多变化,我很难找到解决方案。


学习使用 react-query 包,它会为您处理所有的记忆化。我们可以使用它来处理和扩展大型应用程序。 - man.in.the.jukebox
1个回答

1
当我在不同的路由之间导航时,React Router 会重新渲染记忆化路由,导致 useEffect(() => []) 重新运行并重新获取数据。我想防止这种情况发生,而是保留现有的路由但在 DOM 中隐藏它们。然而,我遇到了一些困难。

简而言之,你无法做到这一点。React 组件会因为以下三个原因之一重新渲染:

  1. 本地组件状态已更新。
  2. 传递的属性值已更新。
  3. 父/祖先组件已更新。

然而,使用 memo 高阶组件在这里不起作用的原因是 Routes 组件仅匹配和渲染单个 Route 组件的 element 属性。从“/”导航到“/b”必然会卸载 MemoizedRouteA 并安装 MemoizedRouteB,反之亦然。这正是 RRD 的预期工作方式,也是 React 组件生命周期的预期工作方式。对组件输出进行记忆化不能处理组件正在被安装/卸载的情况。

如果你真正想要最小化/减少/避免的是组件挂载时重复的异步调用和数据获取/重新获取,那么我建议在这里应用状态提升模式,将状态和useEffect调用移动到父级/祖先组件中。
以下是一个使用Outlet组件和其提供的上下文的简单示例,但状态可以通过任何其他方式提供,例如常规的React上下文或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。我会将其标记为正确,但我真正想要的是类似于v6的react-router-cache-route。每个路由都使用relay从GraphQL加载数据源,并且我希望将其保留在DOM中,以便在浏览器历史记录中前进/后退时Relay的分页光标不会失去同步(当使用Relay的存储和网络获取策略时会发生这种情况)。看起来我需要看看Apollo是否更加稳定。 - Luke Rhodes
@LukeRhodes 我明白了。我对Apollo/GraphQL没有太多经验,但根据我在Redux和Redux-ToolKit Query方面的经验,我认为最好将代码中缓存响应的"Query"部分与路由/导航等应用程序的其他逻辑方面解耦。是在路由上渲染的组件发起/处理数据请求,而"API"代码则负责缓存响应。不同的工具适用于不同的使用情况。 - Drew Reese

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接