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

Next.js 13 App Router服务器组件实现无限滚动分页方案咨询

Next.js 13 App Router 无限滚动分页实现方案

核心思路

初始数据用服务器组件预取(你当前的实现没问题),后续的无限滚动加载有两种主流实现方式,根据技术栈和需求选择:


方案一:客户端组件 + SWR(兼容原有逻辑)

如果想沿用之前SWR/ReactQuery的客户端方案,只需把列表渲染部分拆成客户端组件,由它负责监听滚动、请求后续数据并更新列表。服务器组件仅负责预取第一页数据并传递给客户端组件。

实现代码

  1. 服务器组件 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>
  );
}
  1. 客户端组件 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,在客户端触发滚动时调用服务器端函数获取下一页数据,再追加到列表。

实现代码

  1. 定义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[]>;
}
  1. 整合逻辑的页面组件 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 14:35:34