Next.js 13 App Router服务器组件实现无限滚动分页方案咨询
Next.js 13 App Router 无限滚动分页实现方案
核心思路
初始数据用服务器组件预取(你当前的实现没问题),后续的无限滚动加载有两种主流实现方式,根据技术栈和需求选择:
方案一:客户端组件 + SWR(兼容原有逻辑)
如果想沿用之前SWR/ReactQuery的客户端方案,只需把列表渲染部分拆成客户端组件,由它负责监听滚动、请求后续数据并更新列表。服务器组件仅负责预取第一页数据并传递给客户端组件。
实现代码
- 服务器组件
app/organisations/page.tsx:
import OrganisationInterface from "@/interfaces/OrganisationInterface"; import OrganisationList from "./OrganisationList"; async function getInitialData() { const res = await fetch('http://api.test/v1/organisations?page=1', { cache: 'no-store' }); if (!res.ok) throw new Error('Failed to fetch initial data'); return res.json(); } export default async function Page() { const initialData = await getInitialData(); return ( <div className="mx-auto min-h-screen"> <OrganisationList initialData={initialData} /> </div> ); }
- 客户端组件
app/organisations/OrganisationList.tsx(需加"use client"指令):
"use client"; import { useSWRInfinite } from 'swr'; import OrganisationInterface from "@/interfaces/OrganisationInterface"; import { useEffect, useRef, useState } from 'react'; // SWR 请求函数 const fetcher = async (url: string) => { const res = await fetch(url); if (!res.ok) throw new Error('Failed to fetch data'); return res.json(); }; export default function OrganisationList({ initialData }: { initialData: OrganisationInterface[] }) { const [hasMore, setHasMore] = useState(true); const lastElementRef = useRef<HTMLDivElement>(null); // 配置SWR无限加载 const { data, error, size, setSize } = useSWRInfinite( (pageIndex) => `http://api.test/v1/organisations?page=${pageIndex + 1}`, fetcher, { initialData: [initialData], // 第一页用服务器预取的数据 } ); // 合并所有页的数据 const allOrganisations = data ? data.flat() : []; // 监听滚动,触发加载下一页 useEffect(() => { const observer = new IntersectionObserver( (entries) => { const firstEntry = entries[0]; if (firstEntry.isIntersecting && hasMore && !error) { setSize(size + 1); } }, { threshold: 0.1 } ); if (lastElementRef.current) { observer.observe(lastElementRef.current); } return () => { if (lastElementRef.current) { observer.unobserve(lastElementRef.current); } }; }, [size, hasMore, error]); // 判断是否还有更多数据(根据API返回的条目数判断) useEffect(() => { if (data && data[data.length - 1].length < 10) { setHasMore(false); } }, [data]); if (error) return <div>加载失败</div>; return ( <> {allOrganisations.map((org, index) => ( <div key={org.id} ref={index === allOrganisations.length - 1 ? lastElementRef : null}> {org.attributes.title.lt} </div> ))} {hasMore && <div>加载中...</div>} {!hasMore && <div>没有更多数据了</div>} </> ); }
方案二:Server Actions + 流式渲染(全栈方案)
如果想尽量用服务器端逻辑处理数据请求(避免客户端暴露API细节),可以用Next.js的Server Actions,在客户端触发滚动时调用服务器端函数获取下一页数据,再追加到列表。
实现代码
- 定义Server Action(可放在page.tsx或单独文件):
"use server"; import OrganisationInterface from "@/interfaces/OrganisationInterface"; export async function fetchOrganisationsPage(page: number) { const res = await fetch(`http://api.test/v1/organisations?page=${page}`, { cache: 'no-store' }); if (!res.ok) throw new Error('Failed to fetch page data'); return res.json() as Promise<OrganisationInterface[]>; }
- 整合逻辑的页面组件
app/organisations/page.tsx:
"use client"; import { useState, useEffect, useRef } from 'react'; import OrganisationInterface from "@/interfaces/OrganisationInterface"; import { fetchOrganisationsPage } from './actions'; export default function Page() { const [organisations, setOrganisations] = useState<OrganisationInterface[]>([]); const [currentPage, setCurrentPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [isLoading, setIsLoading] = useState(false); const lastElementRef = useRef<HTMLDivElement>(null); // 初始化加载第一页 useEffect(() => { const loadInitialPage = async () => { const data = await fetchOrganisationsPage(1); setOrganisations(data); setHasMore(data.length === 10); }; loadInitialPage(); }, []); // 加载下一页 const loadNextPage = async () => { if (isLoading || !hasMore) return; setIsLoading(true); try { const nextPage = currentPage + 1; const data = await fetchOrganisationsPage(nextPage); setOrganisations(prev => [...prev, ...data]); setCurrentPage(nextPage); setHasMore(data.length === 10); } catch (err) { console.error('加载失败:', err); } finally { setIsLoading(false); } }; // 滚动监听触发加载 useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore && !isLoading) { loadNextPage(); } }, { threshold: 0.1 } ); if (lastElementRef.current) observer.observe(lastElementRef.current); return () => { if (lastElementRef.current) observer.unobserve(lastElementRef.current); }; }, [hasMore, isLoading]); return ( <div className="mx-auto min-h-screen"> {organisations.map((org, index) => ( <div key={org.id} ref={index === organisations.length - 1 ? lastElementRef : null}> {org.attributes.title.lt} </div> ))} {isLoading && <div>加载中...</div>} {!hasMore && !isLoading && <div>没有更多数据了</div>} </div> ); }
方案选择建议
- 若已熟悉SWR/ReactQuery,优先选方案一,代码改动小,兼容原有逻辑,客户端缓存、重试等特性直接可用。
- 若想减少客户端API依赖、统一数据处理逻辑在服务器端,选方案二,Server Actions可避免在客户端暴露API地址和参数,安全性更高。
内容的提问来源于stack exchange,提问作者RomkaLTU
相关产品推荐
相关产品推荐

