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

Next.js 14 SSR页面结合Apollo GraphQL实现分页的疑问

问题描述

我正在使用Next.js v14、GraphQL、ApolloClient及ApolloGraphQL对NextJS的实验性支持,相关依赖版本如下:

{
    "@apollo/client": "^3.12.0-rc.3",
    "@apollo/experimental-nextjs-app-support": "^0.11.6",
}

我想要制作一个SSR首页展示用户帖子,采用分页模式:初始加载10条数据,用户点击加载更多时获取更多内容,但遇到了两个核心问题:

  • 官方分页文档里的useQuery只适用于CSR组件,而我的SSR页面只能用getClient().query(),但这个方法没有fetchMore()方法。
  • 我自己实现了fetchMore函数,但SSR页面里的按钮没法绑定onClick事件触发它。

我希望尽可能保持数据在服务器端获取,不想直接改成纯CSR页面,有没有可行的解决方案?

相关代码

import { getClient } from '@/lib/apollo';
import { gql } from '@apollo/client';

const GET_ALL_POSTS = gql`
    query GetAllPosts($limit: Int!, $cursor: String) {
        posts(limit: $limit, cursor: $cursor) {
            posts {
                id
                title
            }
            hasMore
        }
    }
`;

export const revalidate = 0;

export default async function Home() {
    const queryResult = await getClient().query({
        query: GET_ALL_POSTS,
        variables: {
            limit: 10,
        },
    });

    // 无关的加载状态处理代码

    const data = queryResult.data?.posts;

    // 示例的加载更多逻辑,但SSR页面中按钮无法绑定onClick
    async function fetchMore() {
        await getClient().query({
            query: GET_ALL_POSTS,
            variables: {
                limit: 10,
                cursor: data.posts[data.posts.length - 1].createdAt,
            },
        });

        getClient().cache.writeQuery({
            query: GET_ALL_POSTS,
            data: {
                posts: {
                    posts: [...data.posts, ...queryResult.data.posts.posts],
                    hasMore: queryResult.data.posts.hasMore,
                },
            },
        });
    }


    return (<div>
        {/* 无关的帖子展示代码 */}
            {queryResult.data && data.hasMore && (
                <button
                    onClick={fetchMore}
                >
                    Load more
                </button>
            )}
    </div>);
}
解决方案

方案1:拆分客户端组件处理交互逻辑

Next.js App Router支持混合服务器/客户端组件,你可以把分页按钮和加载逻辑放到标记了'use client'的组件中,既保留初始数据的SSR渲染,又能正常使用客户端交互和Apollo的fetchMore能力:

  1. 创建客户端组件PostPagination.js:
'use client';

import { useQuery } from '@apollo/client';
import { gql } from '@apollo/client';

const GET_ALL_POSTS = gql`
    query GetAllPosts($limit: Int!, $cursor: String) {
        posts(limit: $limit, cursor: $cursor) {
            posts {
                id
                title
            }
            hasMore
        }
    }
`;

export default function PostPagination({ initialPosts, initialHasMore }) {
    const { data, fetchMore } = useQuery(GET_ALL_POSTS, {
        variables: { limit: 10 },
        initialData: {
            posts: {
                posts: initialPosts,
                hasMore: initialHasMore
            }
        }
    });

    const handleLoadMore = () => {
        if (!data?.posts.hasMore) return;
        
        fetchMore({
            variables: {
                cursor: data.posts.posts[data.posts.posts.length - 1].createdAt
            },
            updateQuery: (prev, { fetchMoreResult }) => {
                if (!fetchMoreResult) return prev;
                return {
                    posts: {
                        ...fetchMoreResult.posts,
                        posts: [...prev.posts.posts, ...fetchMoreResult.posts.posts]
                    }
                };
            }
        });
    };

    return (
        <>
            {data?.posts.posts.map(post => (
                <div key={post.id}>{post.title}</div>
            ))}
            {data?.posts.hasMore && (
                <button onClick={handleLoadMore}>加载更多</button>
            )}
        </>
    );
}
  1. 在服务器端页面中引入该组件:
import { getClient } from '@/lib/apollo';
import { gql } from '@apollo/client';
import PostPagination from './PostPagination';

const GET_ALL_POSTS = gql`
    query GetAllPosts($limit: Int!, $cursor: String) {
        posts(limit: $limit, cursor: $cursor) {
            posts {
                id
                title
            }
            hasMore
        }
    }
`;

export const revalidate = 0;

export default async function Home() {
    const queryResult = await getClient().query({
        query: GET_ALL_POSTS,
        variables: { limit: 10 },
    });

    const initialPosts = queryResult.data.posts.posts;
    const initialHasMore = queryResult.data.posts.hasMore;

    return (
        <div>
            <PostPagination 
                initialPosts={initialPosts} 
                initialHasMore={initialHasMore} 
            />
        </div>
    );
}

这种方式既保留了初始数据的SSR渲染,后续分页请求由客户端Apollo Client处理,自动管理缓存,无需手动写入。

方案2:通过API路由实现全服务器端分页

如果希望所有分页请求都在服务器端执行,可以通过Next.js API路由封装GraphQL查询,客户端通过fetch调用API获取数据:

  1. 创建API路由app/api/posts/route.js:
import { getClient } from '@/lib/apollo';
import { gql } from '@apollo/client';

const GET_ALL_POSTS = gql`
    query GetAllPosts($limit: Int!, $cursor: String) {
        posts(limit: $limit, cursor: $cursor) {
            posts {
                id
                title
            }
            hasMore
        }
    }
`;

export async function GET(request) {
    const { searchParams } = new URL(request.url);
    const limit = parseInt(searchParams.get('limit')) || 10;
    const cursor = searchParams.get('cursor');

    const client = getClient();
    const result = await client.query({
        query: GET_ALL_POSTS,
        variables: { limit, cursor }
    });

    return Response.json(result.data.posts);
}
  1. 页面改为客户端组件(或混合模式)调用API:
'use client';

import { useState, useEffect } from 'react';

export default function Home() {
    const [posts, setPosts] = useState([]);
    const [hasMore, setHasMore] = useState(true);
    const [cursor, setCursor] = useState(null);
    const [loading, setLoading] = useState(false);

    // 初始加载(也可保留SSR初始数据,此处简化为客户端请求)
    useEffect(() => {
        fetchPosts();
    }, []);

    const fetchPosts = async (nextCursor = null) => {
        setLoading(true);
        const res = await fetch(`/api/posts?limit=10${nextCursor ? `&cursor=${nextCursor}` : ''}`);
        const data = await res.json();
        
        if (nextCursor) {
            setPosts(prev => [...prev, ...data.posts]);
        } else {
            setPosts(data.posts);
        }
        setHasMore(data.hasMore);
        if (data.posts.length > 0) {
            setCursor(data.posts[data.posts.length - 1].createdAt);
        }
        setLoading(false);
    };

    return (
        <div>
            {posts.map(post => (
                <div key={post.id}>{post.title}</div>
            ))}
            {hasMore && !loading && (
                <button onClick={() => fetchPosts(cursor)}>加载更多</button>
            )}
            {loading && <p>加载中...</p>}
        </div>
    );
}

若要保留初始数据SSR,可在服务器端页面先获取初始数据,再传递给客户端组件使用。

关键说明

  • Next.js App Router中,无'use client'标记的服务器组件无法绑定客户端事件(如onClick),必须将交互逻辑拆分到客户端组件中。
  • 方案1更贴合Apollo生态,利用客户端缓存和fetchMore能力,兼顾SSR和交互体验。
  • 方案2适合完全依赖服务器端处理查询的场景,所有分页请求都通过API路由在服务器端执行。

内容的提问来源于stack exchange,提问作者randomuser5767124

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 00:24:52