React中点击浏览器返回按钮,如何快速定位列表历史位置并保留数据?
解决方案:React懒加载列表返回位置与缓存优化
一、利用浏览器历史状态(history.state)+ 会话存储(sessionStorage)实现状态恢复
这个方案可以同时解决滚动位置恢复和已加载数据的缓存问题,且对浏览器返回按钮天然支持。
核心逻辑:
- 离开列表页时:将当前已加载的商品数据、当前页码、滚动位置保存到
history.state和sessionStorage中(双重保险,避免history.state丢失)。 - 回到列表页时:组件挂载时优先读取
history.state中的状态,若不存在则读取sessionStorage,直接恢复数据和滚动位置,无需重新请求API。
代码示例:
import { useEffect, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; const ProductList = () => { const [products, setProducts] = useState([]); const [currentPage, setCurrentPage] = useState(1); const history = useHistory(); const location = useLocation(); // 组件挂载/路由变化时恢复状态 useEffect(() => { // 读取保存的状态 const savedState = history.state?.productList || JSON.parse(sessionStorage.getItem('productListState')); if (savedState) { // 恢复已加载数据和页码 setProducts(savedState.products); setCurrentPage(savedState.currentPage); // 等待组件渲染完成后恢复滚动位置 setTimeout(() => { window.scrollTo(0, savedState.scrollTop); }, 0); } else { // 首次进入或无缓存时加载第一页 fetchPage(1); } // 监听页面跳转(比如进入详情页),保存状态 const unblock = history.block((tx) => { if (tx.action === 'PUSH') { const currentState = { products, currentPage, scrollTop: window.scrollY }; // 保存到sessionStorage sessionStorage.setItem('productListState', JSON.stringify(currentState)); // 更新当前路由的history.state history.replace({ ...location, state: { ...location.state, productList: currentState } }); } return true; }); // 监听页面刷新/关闭,保存状态 const handleBeforeUnload = () => { const currentState = { products, currentPage, scrollTop: window.scrollY }; sessionStorage.setItem('productListState', JSON.stringify(currentState)); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { unblock(); window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [history, location]); // 滚动懒加载逻辑 useEffect(() => { const handleScroll = () => { const isBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200; if (isBottom) { fetchPage(currentPage + 1); } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, [currentPage]); // 分页获取数据,优先读取缓存 const fetchPage = async (page) => { // 按页码缓存,避免全量缓存的性能问题 const cachedPage = JSON.parse(sessionStorage.getItem(`productPage_${page}`)); if (cachedPage) { setProducts(prev => [...prev, ...cachedPage]); setCurrentPage(page); return; } // 无缓存时请求API const res = await fetch(`/api/products?page=${page}&limit=10`); const data = await res.json(); setProducts(prev => [...prev, ...data]); setCurrentPage(page); // 缓存单页数据 sessionStorage.setItem(`productPage_${page}`, JSON.stringify(data)); }; return ( <div className="product-list"> {products.map(product => ( <div key={product.id} className="product-item" onClick={() => history.push(`/product/${product.id}`)} > {/* 商品展示内容 */} </div> ))} </div> ); }; export default ProductList;
二、优化Redux缓存方案,适配浏览器返回
如果坚持使用Redux,可以做以下优化:
- 分页存储缓存:不要一次性缓存所有数据,而是按页码将数据存在Redux中(比如
{ page1: [...], page2: [...] }),减少单次缓存的开销。 - 监听路由变化恢复状态:在列表页组件中,通过
useLocation监听路由变化,当从详情页返回时,直接从Redux中取出所有已加载的页码数据合并,同时恢复滚动位置。
关键代码片段:
import { useEffect, useState } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useLocation, useHistory } from 'react-router-dom'; import { fetchProducts, getCachedPages } from './redux/slices/productSlice'; const ProductList = () => { const dispatch = useDispatch(); const location = useLocation(); const history = useHistory(); const cachedPages = useSelector(getCachedPages); const [products, setProducts] = useState([]); const [currentPage, setCurrentPage] = useState(1); useEffect(() => { // 从Redux读取已缓存的所有页码数据 const allProducts = Object.values(cachedPages).flat(); if (allProducts.length > 0) { setProducts(allProducts); setCurrentPage(Object.keys(cachedPages).length); // 恢复滚动位置(需要之前把滚动位置存在sessionStorage) const scrollTop = sessionStorage.getItem('productScrollTop'); scrollTop && setTimeout(() => window.scrollTo(0, Number(scrollTop)), 0); } else { dispatch(fetchProducts(1)); } // 离开页面时保存滚动位置 const handleRouteChange = () => { sessionStorage.setItem('productScrollTop', window.scrollY.toString()); }; const unlisten = history.listen(handleRouteChange); return () => unlisten(); }, [location, cachedPages, dispatch, history]); };
三、浏览器原生滚动恢复的补充
浏览器本身提供了scrollRestoration API,但由于组件重新渲染时数据可能还未加载完成,直接使用可能导致滚动位置不准确。可以配合状态恢复使用:
useEffect(() => { // 禁用浏览器自动滚动恢复,手动控制 history.scrollRestoration = 'manual'; }, [history]);
方案对比
- history.state + sessionStorage:实现简单,无需全局状态管理,对浏览器返回和页面刷新都支持,分页缓存性能更优。
- 优化后的Redux方案:适合需要全局共享列表数据的场景,但需要额外维护路由监听逻辑。
内容的提问来源于stack exchange,提问作者Jayant Singh
相关产品推荐
相关产品推荐

