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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 22:45:45