如何在React+TypeScript项目中基于GraphQL实现高效分页?
基于TypeScript + Apollo Client的React分页最佳实践
1. 解决数据加载延迟,提升用户体验
用Apollo Client的useInfiniteQuery替代useQuery,它专门适配无限分页场景,支持自动跟踪分页状态,还能实现预加载(提前触发下一页请求,减少用户等待感)。
优先采用Cursor分页(比Offset更高效)
如果服务器支持,优先用Cursor分页(Offset分页在数据量大时易出现性能问题和数据重复/遗漏),调整GraphQL查询:
query GetItems($cursor: String, $limit: Int) { items(cursor: $cursor, limit: $limit) { edges { node { id name description } } pageInfo { endCursor hasNextPage } } }
若只能用Offset分页,也可适配useInfiniteQuery,但Cursor是更优方案。
组件中实现无限滚动+预加载
import { useInfiniteQuery } from '@apollo/client'; import { GET_ITEMS } from './queries'; type Item = { id: string; name: string; description: string; }; type ItemsQueryResult = { items: { edges: Array<{ node: Item }>; pageInfo: { endCursor: string; hasNextPage: boolean }; }; }; const ItemsList = () => { const { data, loading, error, fetchNextPage, hasNextPage, } = useInfiniteQuery<ItemsQueryResult>(GET_ITEMS, { variables: { limit: 10 }, // 自动计算下一页参数 getNextPageParam: (lastPage) => { return lastPage.items.pageInfo.hasNextPage ? lastPage.items.pageInfo.endCursor : undefined; }, // 滚动到底部自动加载下一页 onSuccess: () => { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasNextPage) { fetchNextPage(); } }); observer.observe(document.getElementById('load-more-trigger')!); }, }); // 合并所有分页数据 const allItems = data?.pages.flatMap(page => page.items.edges.map(edge => edge.node)) || []; if (loading) return <div>加载中...</div>; if (error) return <div>加载失败</div>; return ( <div> {allItems.map(item => ( <div key={item.id}> <h3>{item.name}</h3> <p>{item.description}</p> </div> ))} {hasNextPage && <div id="load-more-trigger">加载更多...</div>} </div> ); };
2. 优化缓存,避免重复请求
Apollo Client默认缓存查询结果,但分页场景需手动配置缓存合并规则,确保数据不重复且正确累加:
针对Cursor分页的缓存配置
在Apollo Client初始化时添加类型策略:
import { ApolloClient, InMemoryCache } from '@apollo/client'; const client = new ApolloClient({ uri: '/graphql', cache: new InMemoryCache({ typePolicies: { Query: { fields: { items: { // 以limit为缓存分组键,cursor作为分页区分参数 keyArgs: ['limit'], // 合并新数据到现有缓存,自动去重 merge(existing, incoming) { let edges = existing?.edges || []; if (incoming?.edges) { const existingIds = new Set(edges.map(edge => edge.node.id)); edges = [...edges, ...incoming.edges.filter(edge => !existingIds.has(edge.node.id))]; } return { ...incoming, edges, }; }, }, }, }, }, }), });
Offset分页的缓存处理
若使用Offset分页,可通过fetchPolicy: 'cache-first'优先读取缓存,同时手动维护offset状态,避免重复请求相同offset的数据:
const { data } = useQuery(GET_ITEMS, { variables: { offset, limit: 10 }, fetchPolicy: 'cache-first', });
3. 保证分页与缓存的类型安全
手动定义TypeScript类型易出错,推荐用GraphQL Code Generator自动生成所有查询、变量、返回值的类型:
- 安装依赖:
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-apollo-client @graphql-codegen/typescript-operations - 配置
codegen.ts:import type { CodegenConfig } from '@graphql-codegen/cli'; const config: CodegenConfig = { schema: 'http://your-graphql-server-url', documents: './src/**/*.graphql', generates: { './src/generated/graphql.ts': { plugins: ['typescript', 'typescript-operations', 'typescript-apollo-client'], }, }, }; export default config; - 添加npm脚本到
package.json:{ "scripts": { "codegen": "graphql-codegen --config codegen.ts" } } - 运行
npm run codegen生成类型,之后在组件中直接导入使用:import { useInfiniteQuery } from '@apollo/client'; import { GetItemsDocument, GetItemsQuery, GetItemsQueryVariables } from './generated/graphql'; const ItemsList = () => { const { data, loading, error } = useInfiniteQuery<GetItemsQuery, GetItemsQueryVariables>( GetItemsDocument, { variables: { limit: 10 } } ); // 所有类型自动匹配,不会出现类型不匹配问题 const allItems = data?.pages.flatMap(page => page.items?.edges?.map(edge => edge.node) || []) || []; // ... 渲染逻辑 };
内容的提问来源于stack exchange,提问作者maxtymosh
相关产品推荐
相关产品推荐

