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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 08:16:03