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能力:
- 创建客户端组件
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> )} </> ); }
- 在服务器端页面中引入该组件:
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获取数据:
- 创建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); }
- 页面改为客户端组件(或混合模式)调用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
相关产品推荐
相关产品推荐

