You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Router Dom v6:返回页面时如何避免列表页重渲染及重复请求

解决React Router v6返回列表页重复请求数据的问题

问题场景

我有两个页面:

  1. customer-list(客户列表页):以表格形式展示客户ID列表,点击按钮可跳转到客户详情页
  2. customer-detail(客户详情页):带有返回按钮,点击可回到客户列表页

当前代码及路由结构如下:

客户列表页代码

const CustomerList = () => {
   const [customers, setCustomers] = useState([]);
   useEffect(() => { /* 此处请求客户列表数据 */ }, [])

   return (
      { 
         customers.map(c => 
            <tr>
              <td>{customer.customer_id}</td>
              <td><button onClick={() => navigate(`/customer/${customer.customer_id}`)}>View details</button></td>
           </tr>
         ) 
      }
   );
}

客户详情页代码

const CustomerDetails = () => {
   const navigate = useNavigate();

   return (
       <>
        <h2>Customer Details</h2>
        <button onClick={() => navigate(-1)}>Back</button>
       </>
   );
}

路由结构

/ -> root customer-list
/customer/:customerId -> customer-details

核心问题:返回客户列表页时,页面会完全重渲染并重新请求全部数据,导致加载耗时过长,需要优化体验避免重复加载。


可行解决方案

1. 将列表数据提升到路由上层组件缓存

把客户列表的状态管理和数据请求逻辑移到包裹两个页面的父组件(比如App或专门的路由布局组件),这样切换页面时父组件不会卸载,数据会被保留下来。

示例代码:

// 父组件(如App.jsx)
function App() {
  const [customers, setCustomers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchCustomers = async () => {
      try {
        const res = await fetch('/api/customers');
        const data = await res.json();
        setCustomers(data);
      } catch (err) {
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    };
    // 仅在父组件首次挂载时请求数据
    fetchCustomers();
  }, []);

  return (
    <Routes>
      <Route path="/" element={<CustomerList customers={customers} isLoading={isLoading} />} />
      <Route path="/customer/:customerId" element={<CustomerDetails />} />
    </Routes>
  );
}

// 修改后的CustomerList组件
const CustomerList = ({ customers, isLoading }) => {
  const navigate = useNavigate();

  if (isLoading) return <div>Loading...</div>;

  return (
    <table>
      <tbody>
        {customers.map(customer => (
          <tr key={customer.customer_id}>
            <td>{customer.customer_id}</td>
            <td>
              <button onClick={() => navigate(`/customer/${customer.customer_id}`)}>View details</button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

2. 使用嵌套路由保持列表组件挂载

调整路由结构,把列表页作为父路由,详情页作为嵌套路由。这样切换到详情页时,列表组件不会被卸载,只是隐藏,返回时直接展示缓存内容。

调整后的路由结构:

/ -> CustomerList(包含Outlet)
/customer/:customerId -> CustomerDetails(渲染在Outlet中)

示例代码:

// 修改路由配置
function App() {
  return (
    <Routes>
      <Route path="/" element={<CustomerList />}>
        <Route path="customer/:customerId" element={<CustomerDetails />} />
      </Route>
    </Routes>
  );
}

// 修改后的CustomerList组件
const CustomerList = () => {
  const [customers, setCustomers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const navigate = useNavigate();
  const outlet = useOutlet(); // 获取嵌套路由内容

  useEffect(() => {
    const fetchCustomers = async () => {
      const res = await fetch('/api/customers');
      const data = await res.json();
      setCustomers(data);
      setIsLoading(false);
    };
    fetchCustomers();
  }, []);

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      {/* 始终渲染列表 */}
      <table>
        <tbody>
          {customers.map(customer => (
            <tr key={customer.customer_id}>
              <td>{customer.customer_id}</td>
              <td>
                <button onClick={() => navigate(`customer/${customer.customer_id}`)}>View details</button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
      {/* 渲染嵌套的详情页 */}
      {outlet}
    </div>
  );
};

// 修改后的CustomerDetails组件
const CustomerDetails = () => {
  const navigate = useNavigate();

  return (
    <div className="customer-detail">
      <h2>Customer Details</h2>
      <button onClick={() => navigate('..')}>Back</button>
      {/* 详情内容 */}
    </div>
  );
};

3. 使用本地存储缓存数据

如果希望页面刷新后仍能保留缓存,可以把请求到的客户列表存入LocalStorage或SessionStorage,下次加载时优先读取本地数据,再按需更新。

示例代码:

const CustomerList = () => {
  const [customers, setCustomers] = useState([]);
  const [isLoading, setIsLoading] = useState(true);
  const navigate = useNavigate();

  useEffect(() => {
    const fetchCustomers = async () => {
      // 先读取本地缓存
      const cachedCustomers = localStorage.getItem('customerList');
      if (cachedCustomers) {
        setCustomers(JSON.parse(cachedCustomers));
        setIsLoading(false);
      }

      // 可选:请求最新数据更新缓存
      try {
        const res = await fetch('/api/customers');
        const data = await res.json();
        setCustomers(data);
        localStorage.setItem('customerList', JSON.stringify(data));
      } catch (err) {
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    };
    fetchCustomers();
  }, []);

  // 可选:组件卸载时清除缓存(根据业务需求决定)
  useEffect(() => {
    return () => {
      // localStorage.removeItem('customerList');
    };
  }, []);

  if (isLoading) return <div>Loading...</div>;

  return (
    <table>
      <tbody>
        {customers.map(customer => (
          <tr key={customer.customer_id}>
            <td>{customer.customer_id}</td>
            <td>
              <button onClick={() => navigate(`/customer/${customer.customer_id}`)}>View details</button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

4. 封装自定义Hook实现带过期时间的缓存

把数据请求和缓存逻辑封装成自定义Hook,支持设置缓存过期时间,复用性更强。

示例代码:

// 自定义Hook:useCachedFetch
function useCachedFetch(key, url, expireTime = 3600000) { // 默认缓存1小时
  const [data, setData] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      const cachedItem = localStorage.getItem(key);
      if (cachedItem) {
        const { data: cachedData, timestamp } = JSON.parse(cachedItem);
        // 判断缓存是否过期
        if (Date.now() - timestamp < expireTime) {
          setData(cachedData);
          setIsLoading(false);
          return;
        }
      }

      // 请求最新数据并更新缓存
      try {
        const res = await fetch(url);
        const newData = await res.json();
        setData(newData);
        localStorage.setItem(key, JSON.stringify({
          data: newData,
          timestamp: Date.now()
        }));
      } catch (err) {
        console.error(err);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, [key, url, expireTime]);

  return { data, isLoading };
}

// 在CustomerList中使用
const CustomerList = () => {
  const { data: customers, isLoading } = useCachedFetch('customerList', '/api/customers');
  const navigate = useNavigate();

  if (isLoading) return <div>Loading...</div>;
  if (!customers) return <div>Failed to load data</div>;

  return (
    <table>
      <tbody>
        {customers.map(customer => (
          <tr key={customer.customer_id}>
            <td>{customer.customer_id}</td>
            <td>
              <button onClick={() => navigate(`/customer/${customer.customer_id}`)}>View details</button>
            </td>
          </tr>
        ))}
      </tbody>
    </table>
  );
};

内容的提问来源于stack exchange,提问作者Patrick Starfish

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 19:43:15