Next.js App Router无限滚动返回丢失位置与加载数据问题求助
Next.js App Router 无限滚动返回页面状态丢失解决方案
问题根源
App Router 默认在从详情页返回列表页时会重新渲染页面组件,若无限滚动的加载状态、已加载数据未做持久化,就会触发数据重新请求、滚动位置重置。之前直接传入前两页数据能保留状态,是因为初始数据为服务端渲染/静态生成,不依赖客户端动态加载的状态。
具体解决方法
1. 手动持久化数据与滚动位置
用 localStorage 保存已加载商品列表和滚动位置,页面初始化时读取恢复:
'use client' import { useState, useEffect } from 'react' import loadMore from './loadMore' const PAGE_SIZE = 20 // 替换为你的每页商品数量 export default function ProductsPage() { const [products, setProducts] = useState([]) const [page, setPage] = useState(1) const [isLoading, setIsLoading] = useState(false) useEffect(() => { // 恢复缓存的状态 const savedProducts = localStorage.getItem('infiniteProducts') const savedScrollTop = localStorage.getItem('scrollTop') if (savedProducts) { const parsedProducts = JSON.parse(savedProducts) setProducts(parsedProducts) // 计算已加载的页数,避免重复请求 setPage(Math.ceil(parsedProducts.length / PAGE_SIZE)) } if (savedScrollTop) { window.scrollTo(0, parseInt(savedScrollTop)) } // 页面卸载时保存状态 return () => { localStorage.setItem('infiniteProducts', JSON.stringify(products)) localStorage.setItem('scrollTop', window.scrollY.toString()) } }, [products]) // 无限滚动监听逻辑 useEffect(() => { const handleScroll = async () => { const shouldLoad = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200 if (shouldLoad && !isLoading) { setIsLoading(true) const newProducts = await loadMore(page + 1) setProducts(prev => [...prev, ...newProducts]) setPage(prev => prev + 1) setIsLoading(false) } } window.addEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll) }, [page, isLoading]) // 渲染商品列表 return ( <div className="product-list"> {products.map(product => ( <a key={product.id} href={`/products/${product.id}`} className="product-item"> {/* 商品内容渲染 */} </a> ))} {isLoading && <div className="loading">加载中...</div>} </div> ) }
注意:若商品数据有更新,需在页面刷新或检测到数据版本变化时清空
localStorage缓存,避免展示过时内容。
2. 用 SWR/React Query 简化缓存逻辑
这类数据请求库默认会缓存请求结果,返回页面时直接读取缓存,无需重新请求,同时支持无限滚动的便捷实现:
以 SWR 为例:
'use client' import useSWRInfinite from 'swr/infinite' import { useEffect } from 'react' // 生成请求 key,控制分页请求 const getRequestKey = (pageIndex, previousPageData) => { if (previousPageData && !previousPageData.length) return null // 无更多数据 return `/api/products?page=${pageIndex + 1}` } // 请求函数 const fetcher = async (url) => { const res = await fetch(url) return res.json() } export default function ProductsPage() { const { data, error, size, setSize, isLoading } = useSWRInfinite(getRequestKey, fetcher) // 合并所有分页数据 const products = data ? [].concat(...data) : [] const hasMore = data && data[data.length - 1]?.length > 0 // 恢复滚动位置 useEffect(() => { const savedScrollTop = localStorage.getItem('scrollTop') if (savedScrollTop) { window.scrollTo(0, parseInt(savedScrollTop)) } // 离开页面时保存滚动位置 return () => { localStorage.setItem('scrollTop', window.scrollY.toString()) } }, []) // 滚动加载更多 useEffect(() => { const handleScroll = () => { const shouldLoad = window.innerHeight + window.scrollY >= document.body.offsetHeight - 200 if (shouldLoad && hasMore && !isLoading) { setSize(size + 1) } } window.addEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll) }, [size, hasMore, isLoading]) if (error) return <div>加载失败</div> // 渲染商品列表 return ( <div className="product-list"> {products.map(product => ( <a key={product.id} href={`/products/${product.id}`} className="product-item"> {/* 商品内容渲染 */} </a> ))} {isLoading && <div className="loading">加载中...</div>} </div> ) }
3. 避免组件不必要重挂载
确保列表页组件不会因路由变化被卸载:
- 若列表页是嵌套路由,可将状态提升到父组件或用 Context 管理全局状态
- 使用
usePathname监听路由变化,回到列表页时主动恢复状态
总结
核心是持久化已加载数据和滚动位置,手动用 localStorage 或借助 SWR/React Query 都能解决问题;同时要避免组件不必要的重挂载,确保返回页面时能快速恢复之前的状态。
内容的提问来源于stack exchange,提问作者kahen official
相关产品推荐
相关产品推荐

