GraphQL与React:游标分页fetchMore实现正确性及优化咨询
确认Apollo Client分页fetchMore实现是否正确及优化方案
问题描述
我需要确认当前的分页实现是否正确,因为感觉存在问题。我的需求是:
- 点击
fetchMore按钮时,页面自动更新并展示新的查询结果。
实现代码
getPosts.ts
import { gql } from '@apollo/client' export const GET_POSTS = gql` query GetPosts( $after: String $filterBy: [PostListFilterByInput!] $limit: Int! $offset: Int $orderByString: String $reverse: Boolean $spaceIds: [ID!] ) { posts( after: $after filterBy: $filterBy limit: $limit offset: $offset orderByString: $orderByString reverse: $reverse spaceIds: $spaceIds ) { totalCount pageInfo { hasNextPage endCursor } edges { node { id title description reactionsCount fields { key value relationEntities { medias { ... on Image { urls { medium } } } } } } } } } `
usePosts.ts Hook
import { useQuery } from '@apollo/client' import { GetPostsQuery, GetPostsQueryVariables, Image } from '@/graphql/generated/types' import { GRAPHQL_VARIABLES } from '@/constants/graphql' import { GET_POSTS } from '@/graphql/queries/posts/getPosts' export interface IPost { id: string title: string description: string reactionsCount: number urls: { medium?: string } } export const useGetPosts = () => { const { data, loading, error, fetchMore } = useQuery< GetPostsQuery, GetPostsQueryVariables >(GET_POSTS, { variables: { ...GRAPHQL_VARIABLES, after: '' } }) const posts: IPost[] = data?.posts?.edges?.map(({ node }) => { let urls = {} if (node.fields) { for (const field of node.fields) { if (field.key === 'cover_image' && field.relationEntities?.medias) { const image = field.relationEntities.medias.find( (media) => media.__typename === 'Image' ) as Image if (image) { urls = image.urls || {} } } } } return { id: node.id || '', title: node.title || '', description: node.description || '', reactionsCount: node.reactionsCount, urls } }) || [] const hasNextPage = data?.posts?.pageInfo?.hasNextPage const endCursor = data?.posts?.pageInfo?.endCursor return { posts, loading, error, fetchMore, hasNextPage, endCursor } }
Gallery.tsx 组件
import GalleryItem from '@/components/feature/GalleryItem' import { Button } from '@/components/base/Button' import { useGetPosts } from './hooks/usePosts' const Gallery: React.FC = () => { const { posts, fetchMore, hasNextPage, endCursor } = useGetPosts() const loadMore = () => { fetchMore({ variables: { after: endCursor } }) } return ( <> <div className="flex justify-center"> <div className="m-8 grid max-w-[1100px] grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3"> {posts.map((post) => ( <GalleryItem key={post?.id} post={post} /> ))} </div> </div> <div className="mt-10 flex justify-center"> {hasNextPage && ( <Button className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700" onClick={loadMore} > Show More </Button> )} </div> </> ) } export default Gallery
cache.ts GraphQL缓存配置
import { InMemoryCache } from '@apollo/client' export const cache = new InMemoryCache({ typePolicies: { Query: { fields: { posts: { keyArgs: false, merge(existing = { edges: [], pageInfo: {} }, incoming) { const mergedEdges = [...existing.edges, ...incoming.edges] return { ...incoming, edges: mergedEdges } } } } } } })
问题
请问这样的实现是否正确?或者有没有更优的方式在执行fetchMore请求后合并数据?
实现正确性分析
你的当前实现整体方向是正确的,但存在几个细节问题可能导致分页异常:
初始
after变量设置问题
你初始请求时把after设为空字符串'',但GraphQL规范中after通常接受游标值(非空字符串或null)。如果后端期望null表示第一页,空字符串可能导致请求异常,建议改为after: null。合并策略的小缺陷
当前merge函数用...incoming覆盖totalCount和pageInfo没问题,但如果后续有其他字段需要保留可能丢失。可以优化默认值和字段保留逻辑:merge(existing = { totalCount: 0, pageInfo: { hasNextPage: false, endCursor: null }, edges: [] }, incoming) { return { ...incoming, edges: [...(existing.edges || []), ...(incoming.edges || [])], // 若后端返回totalCount为总数据量,保留existing值避免覆盖;若为实时更新则用incoming totalCount: existing.totalCount || incoming.totalCount } }fetchMore变量传递完整性
调用fetchMore时只传after,虽然Apollo会自动合并原变量,但显式传递所有必要变量能避免潜在丢失问题,尤其是原变量动态变化时:const loadMore = () => { fetchMore({ variables: { ...GRAPHQL_VARIABLES, after: endCursor } }) }
更优的数据合并方式
方式1:使用内置concatPagination工具
Apollo Client提供了concatPagination助手函数,专门处理cursor-based分页的edges合并,无需手动写merge:
import { InMemoryCache, concatPagination } from '@apollo/client' export const cache = new InMemoryCache({ typePolicies: { Query: { fields: { posts: concatPagination() } } } })
该工具会自动处理edges合并和pageInfo更新,逻辑和你手动实现的一致,但更简洁。
方式2:自定义merge时添加数据去重
如果后端可能返回重复节点,可以在merge时通过id去重:
merge(existing = { edges: [], pageInfo: {}, totalCount: 0 }, incoming) { const existingIds = new Set(existing.edges.map(edge => edge.node.id)) const newEdges = incoming.edges.filter(edge => !existingIds.has(edge.node.id)) return { ...incoming, edges: [...existing.edges, ...newEdges], totalCount: incoming.totalCount } }
额外优化建议
- 区分加载状态:
fetchMore时loading会再次变为true,可以在组件中区分初始加载和加载更多状态,避免重复显示全局加载动画。 - 捕获fetchMore错误:在调用时添加错误处理,提示用户加载失败:
const loadMore = async () => { try { await fetchMore({ variables: { after: endCursor } }) } catch (err) { console.error('加载更多失败:', err) // 这里可以添加用户提示逻辑 } } - 避免无效请求:调用
fetchMore前判断endCursor和hasNextPage是否有效:const loadMore = () => { if (endCursor && hasNextPage) { fetchMore({ variables: { after: endCursor } }) } }
内容的提问来源于stack exchange,提问作者Daltron
相关产品推荐
相关产品推荐

