UI集成与数据绑定模式


UI集成与数据绑定模式

React组件与GraphQL查询的映射

GraphQL的声明式特性与React的声明式渲染天然契合。一个常见的模式是:每个组件只关心自己需要的数据,通过Fragment声明数据需求,父组件组合子组件的Fragment形成完整查询。

// PostAuthor: 只关心作者信息 const PostAuthor = ({ authorId }: { authorId: string }) => { const { data } = useQuery(gql` query GetAuthor($id: ID!) { user(id: $id) { id name avatarUrl } } `, { variables: { id: authorId } }); return data ? ( <div className="author"> <img src={data.user.avatarUrl} alt={data.user.name} /> <span>{data.user.name}</span> </div> ) : null; }; // PostCard: 组合使用 const PostCard = ({ postId }: { postId: string }) => { const { data, loading } = useQuery(gql` query GetPost($id: ID!) { post(id: $id) { id title content likesCount createdAt authorId } } `, { variables: { id: postId } }); if (loading) return <SkeletonCard />; return data ? ( <article> <h3>{data.post.title}</h3> <p>{data.post.content}</p> <div className="meta"> <PostAuthor authorId={data.post.authorId} /> <span>{data.post.likesCount} 赞</span> </div> </article> ) : null; }; ​

但这种模式有一个问题:PostCard 和 PostAuthor 各发一个请求,导致N+1问题重现。更好的做法是用Fragment让Apollo自动合并查询。

Fragment组合模式

// PostAuthorFragment: 声明作者Fragment const PostAuthorFragment = gql` fragment PostAuthorFragment on User { id name avatarUrl } `; // PostCardFragment: 引用PostAuthorFragment const PostCardFragment = gql` fragment PostCardFragment on Post { id title content likesCount createdAt author { ...PostAuthorFragment } } ${PostAuthorFragment} `; // 父组件:合并Fragment,一次查询 function PostFeed() { const { data } = useQuery(gql` query GetFeed($limit: Int = 10) { posts(limit: $limit) { ...PostCardFragment } } ${PostCardFragment} `, { variables: { limit: 10 } }); return ( <div> {data?.posts.map(post => ( <PostCardItem key={post.id} post={post} /> ))} </div> ); } // 子组件:只使用传入的数据,不发请求 function PostCardItem({ post }) { return ( <article> <h3>{post.title}</h3> <div className="author"> <img src={post.author.avatarUrl} alt={post.author.name} /> <span>{post.author.name}</span> </div> </article> ); } ​

这种模式的关键是:子组件不自己发请求,数据通过props传递。查询在父组件发,Fragment确保数据包含子组件需要的所有字段。这样所有组件的数据需求合并成一个查询,只发一次请求。

分页集成

基于游标的分页是GraphQL推荐的分页方式。Apollo Client的 usePaginationFragment hook简化了分页逻辑:

import { usePaginationFragment } from '@apollo/client'; function PostFeed() { const { data, loading, hasMore, loadMore } = usePaginationFragment( gql` query FeedPagination($count: Int!) { posts(first: $count) { ...Feed_posts } } fragment Feed_posts on PostConnection { edges { node { id title author { name } } cursor } pageInfo { hasNextPage endCursor } } `, {} ); return ( <div> {data.posts.edges.map(({ node }) => ( <PostItem key={node.id} post={node} /> ))} {hasMore && ( <button onClick={() => loadMore(10)} disabled={loading}> 加载更多 </button> )} </div> ); } ​

usePaginationFragment 自动处理游标传递、数据合并、加载状态管理。

乐观更新(Optimistic UI)

乐观更新让UI在等待服务端响应时就展示预期结果,提升用户感知速度:

const [toggleLike] = useMutation(LIKE_POST, { optimisticResponse: ({ postId }) => ({ toggleLike: { __typename: 'ToggleLikePayload', post: { __typename: 'Post', id: postId, isLiked: true, likesCount: post.likesCount + 1, } } }), onError: () => { // 乐观更新失败,回滚由Apollo自动处理 toast.error('操作失败,请重试'); } }); function LikeButton({ post }) { return ( <button onClick={() => toggleLike({ variables: { postId: post.id } })} className={post.isLiked ? 'liked' : ''} > {post.isLiked ? '已赞' : '赞'} ({post.likesCount}) </button> ); } ​

乐观更新适合"大概率成功"的操作(点赞、关注、收藏)。如果操作失败,Apollo Client会自动回滚到之前的状态。不适合"可能失败率较高"的操作(支付、提交表单),因为回滚的视觉跳动会让用户困惑。

错误处理模式

GraphQL的部分执行特性要求客户端做好字段的null检查。推荐的错误处理模式:

function PostDetail({ postId }) { const { data, error } = useQuery(GET_POST, { variables: { id: postId } }); // 网络级错误(没有data返回) if (error && !data?.post) { return <ErrorPage message={error.message} />; } // 加载中 if (!data) return <Loading />; // 部分错误(有data但有errors) // 特定字段可能为null,需要检查 return ( <article> <h1>{data.post.title}</h1> {/* comments可能因为服务端错误而为null */} {data.post.comments ? ( <CommentList comments={data.post.comments} /> ) : ( <div className="error-hint">评论加载失败,请刷新重试</div> )} </article> ); } ​

原则:区分"致命错误"(整个请求失败)和"部分错误"(某些字段失败)。致命错误显示错误页面,部分错误降级处理。

框架差异:不止 React

GraphQL 的数据绑定思想在 Vue、Svelte、Angular 里同样成立。Vue 用 useQuery 返回的响应式数据与模板绑定;Svelte 用 store 封装客户端;Angular 用 Apollo Angular 的 watchQuery 配合异步管道。共同的模式是:组件声明数据需求,客户端库负责拉取、缓存、更新,框架层只做渲染。所以这一节的 Fragment 组合、分页、乐观更新模式可以平移到任何框架,只是 API 名称不同。

什么时候不该用"组件自取数据"

Fragment 组合模式适合数据依赖树稳定的页面。但如果一个组件的数据依赖只在极少数场景出现(比如管理后台的某个只读统计卡片),让它自己发一个独立查询反而更简单——避免为了"合并成一个请求"把 Fragment 树搭得过度复杂。取舍标准是请求数量是否可控、数据依赖是否稳定:请求数量少、依赖稳定,走 Fragment 合并;否则独立查询也可以接受。

图:Fragment组合与查询合并

图:Fragment组合与查询合并

作者与出处
原作者:灏天文库
来源:平台策划编纂
整理人: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U