React Router Dom v6:返回页面时如何避免列表页重渲染及重复请求
解决React Router v6返回列表页重复请求数据的问题
问题场景
我有两个页面:
customer-list(客户列表页):以表格形式展示客户ID列表,点击按钮可跳转到客户详情页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
相关产品推荐
相关产品推荐

