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

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 }
}
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请求后合并数据?


实现正确性分析

你的当前实现整体方向是正确的,但存在几个细节问题可能导致分页异常:

  1. 初始after变量设置问题
    你初始请求时把after设为空字符串'',但GraphQL规范中after通常接受游标值(非空字符串或null)。如果后端期望null表示第一页,空字符串可能导致请求异常,建议改为after: null。

  2. 合并策略的小缺陷
    当前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
      }
    }
    
  3. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 13:55:53