名称: relay-fragments-patterns 用户可调用: false 描述: 使用Relay片段组合、数据掩码、共位和容器模式来构建React应用程序。 允许工具:
- 读
- 写
- 编辑
- 搜索
- 全局
- 终端
Relay片段模式
掌握Relay的片段组合,用于构建可维护的React应用程序,具有正确的数据依赖和组件共位。
概述
Relay片段允许组件级数据声明,具有自动组合、数据掩码和优化数据获取。片段将数据需求与组件共位,以提高可维护性。
安装和设置
安装Relay
# 安装Relay包
npm install react-relay relay-runtime
# 安装Relay编译器
npm install --save-dev relay-compiler babel-plugin-relay
# 安装GraphQL
npm install graphql
Relay配置
// relay.config.js
module.exports = {
src: './src',
schema: './schema.graphql',
exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**'],
language: 'typescript',
artifactDirectory: './src/__generated__'
};
// package.json
{
"scripts": {
"relay": "relay-compiler",
"relay:watch": "relay-compiler --watch"
}
}
环境设置
// RelayEnvironment.js
import {
Environment,
Network,
RecordSource,
Store
} from 'relay-runtime';
function fetchQuery(operation, variables) {
return fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
query: operation.text,
variables
})
}).then(response => response.json());
}
const environment = new Environment({
network: Network.create(fetchQuery),
store: new Store(new RecordSource())
});
export default environment;
核心模式
1. 基础片段定义
// PostCard.jsx
import { graphql, useFragment } from 'react-relay';
const PostCardFragment = graphql`
fragment PostCard_post on Post {
id
title
excerpt
publishedAt
author {
name
avatar
}
}
`;
function PostCard({ post }) {
const data = useFragment(PostCardFragment, post);
return (
<article>
<h2>{data.title}</h2>
<p>{data.excerpt}</p>
<div>
<img src={data.author.avatar} alt={data.author.name} />
<span>{data.author.name}</span>
</div>
<time>{data.publishedAt}</time>
</article>
);
}
export default PostCard;
2. 片段组合
// UserProfile.jsx
const UserProfileFragment = graphql`
fragment UserProfile_user on User {
id
name
bio
...UserAvatar_user
...UserStats_user
}
`;
function UserProfile({ user }) {
const data = useFragment(UserProfileFragment, user);
return (
<div>
<h1>{data.name}</h1>
<p>{data.bio}</p>
<UserAvatar user={data} />
<UserStats user={data} />
</div>
);
}
// UserAvatar.jsx
const UserAvatarFragment = graphql`
fragment UserAvatar_user on User {
name
avatar
isOnline
}
`;
function UserAvatar({ user }) {
const data = useFragment(UserAvatarFragment, user);
return (
<div className="avatar-container">
<img src={data.avatar} alt={data.name} />
{data.isOnline && <span className="online-indicator" />}
</div>
);
}
// UserStats.jsx
const UserStatsFragment = graphql`
fragment UserStats_user on User {
postsCount
followersCount
followingCount
}
`;
function UserStats({ user }) {
const data = useFragment(UserStatsFragment, user);
return (
<div className="stats">
<div>帖子数: {data.postsCount}</div>
<div>粉丝数: {data.followersCount}</div>
<div>关注数: {data.followingCount}</div>
</div>
);
}
3. 片段参数
// Post.jsx
const PostFragment = graphql`
fragment Post_post on Post
@argumentDefinitions(
includeComments: { type: "Boolean!", defaultValue: false }
commentsFirst: { type: "Int", defaultValue: 10 }
) {
id
title
body
comments(first: $commentsFirst) @include(if: $includeComments) {
edges {
node {
...Comment_comment
}
}
}
}
`;
function Post({ post, showComments = false }) {
const data = useFragment(
PostFragment,
post,
{
includeComments: showComments,
commentsFirst: 20
}
);
return (
<article>
<h1>{data.title}</h1>
<div>{data.body}</div>
{showComments && (
<CommentsList comments={data.comments.edges.map(e => e.node)} />
)}
</article>
);
}
4. 数据掩码
// 父组件
const ParentFragment = graphql`
fragment Parent_data on Query {
user {
id
...Child_user
}
}
`;
function Parent({ data }) {
const parentData = useFragment(ParentFragment, data);
// parentData.user 仅包含 id 和片段引用
// 这里无法访问 user.name(数据掩码)
return (
<div>
<h1>用户ID: {parentData.user.id}</h1>
<Child user={parentData.user} />
</div>
);
}
// 子组件
const ChildFragment = graphql`
fragment Child_user on User {
name
email
avatar
}
`;
function Child({ user }) {
const data = useFragment(ChildFragment, user);
// 只有子组件可以访问 name、email、avatar
return (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
<img src={data.avatar} alt={data.name} />
</div>
);
}
5. 带有连接的片段
// PostsList.jsx
const PostsListFragment = graphql`
fragment PostsList_query on Query
@argumentDefinitions(
first: { type: "Int", defaultValue: 10 }
after: { type: "String" }
)
@refetchable(queryName: "PostsListRefetchQuery") {
posts(first: $first, after: $after)
@connection(key: "PostsList_posts") {
edges {
node {
id
...PostCard_post
}
}
}
}
`;
function PostsList({ query }) {
const { data, loadNext, hasNext, isLoadingNext } = usePaginationFragment(
PostsListFragment,
query
);
return (
<div>
{data.posts.edges.map(({ node }) => (
<PostCard key={node.id} post={node} />
))}
{hasNext && (
<button
onClick={() => loadNext(10)}
disabled={isLoadingNext}
>
{isLoadingNext ? '加载中...' : '加载更多'}
</button>
)}
</div>
);
}
6. 可重新获取的片段
// UserProfile.jsx
const UserProfileFragment = graphql`
fragment UserProfile_user on User
@refetchable(queryName: "UserProfileRefetchQuery") {
id
name
bio
posts(first: 10) {
edges {
node {
id
title
}
}
}
}
`;
function UserProfile({ user }) {
const [data, refetch] = useRefetchableFragment(
UserProfileFragment,
user
);
const handleRefresh = () => {
refetch({}, { fetchPolicy: 'network-only' });
};
return (
<div>
<button onClick={handleRefresh}>刷新</button>
<h1>{data.name}</h1>
<p>{data.bio}</p>
<PostsList posts={data.posts.edges} />
</div>
);
}
7. 复数片段
// PostsList.jsx
const PostsListFragment = graphql`
fragment PostsList_posts on Post @relay(plural: true) {
id
title
excerpt
}
`;
function PostsList({ posts }) {
const data = useFragment(PostsListFragment, posts);
return (
<div>
{data.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
// 使用
const query = graphql`
query PostsQuery {
posts {
...PostsList_posts
}
}
`;
8. 条件片段
// Content.jsx
const ContentFragment = graphql`
fragment Content_content on Content {
__typename
... on Post {
title
body
author {
name
}
}
... on Video {
title
duration
thumbnailUrl
creator {
name
}
}
... on Image {
title
imageUrl
photographer {
name
}
}
}
`;
function Content({ content }) {
const data = useFragment(ContentFragment, content);
switch (data.__typename) {
case 'Post':
return (
<article>
<h2>{data.title}</h2>
<p>{data.body}</p>
<span>作者:{data.author.name}</span>
</article>
);
case 'Video':
return (
<div>
<video src={data.thumbnailUrl} />
<h2>{data.title}</h2>
<span>时长:{data.duration}秒</span>
<span>创作者:{data.creator.name}</span>
</div>
);
case 'Image':
return (
<figure>
<img src={data.imageUrl} alt={data.title} />
<figcaption>
{data.title} 摄影师:{data.photographer.name}
</figcaption>
</figure>
);
default:
return null;
}
}
9. 片段容器传统模式
// 传统容器模式(v1-11)
import { createFragmentContainer, graphql } from 'react-relay';
class PostCard extends React.Component {
render() {
const { post } = this.props;
return (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
);
}
}
export default createFragmentContainer(PostCard, {
post: graphql`
fragment PostCard_post on Post {
id
title
excerpt
}
`
});
// 现代钩子模式(v12+)
function PostCard({ post }) {
const data = useFragment(
graphql`
fragment PostCard_post on Post {
id
title
excerpt
}
`,
post
);
return (
<article>
<h2>{data.title}</h2>
<p>{data.excerpt}</p>
</article>
);
}
10. 高级片段模式
// 递归片段
const CommentFragment = graphql`
fragment Comment_comment on Comment {
id
body
author {
name
}
replies(first: 5) {
edges {
node {
...Comment_comment
}
}
}
}
`;
function Comment({ comment, depth = 0 }) {
const data = useFragment(CommentFragment, comment);
if (depth > 3) return null; // 防止无限递归
return (
<div style={{ marginLeft: depth * 20 }}>
<p>{data.body}</p>
<span>{data.author.name}</span>
{data.replies?.edges.map(({ node }) => (
<Comment key={node.id} comment={node} depth={depth + 1} />
))}
</div>
);
}
// 带内联数据的片段
const PostWithInlineFragment = graphql`
fragment PostWithInline_post on Post {
id
title
author {
... on User {
id
name
isVerified
}
... on Organization {
id
name
type
}
}
}
`;
// 带有指令的片段
const ConditionalFragment = graphql`
fragment Conditional_post on Post
@argumentDefinitions(
includeLikes: { type: "Boolean!", defaultValue: false }
includeComments: { type: "Boolean!", defaultValue: true }
) {
id
title
likesCount @include(if: $includeLikes)
comments(first: 10) @include(if: $includeComments) {
edges {
node {
id
body
}
}
}
}
`;
// 带必需字段的片段
const RequiredFieldsFragment = graphql`
fragment RequiredFields_user on User {
id
name @required(action: LOG)
email @required(action: THROW)
avatar @required(action: NONE)
}
`;
最佳实践
- 将片段与组件共位 - 保持数据需求在一起
- 使用片段组合 - 从简单片段构建复杂查询
- 利用数据掩码 - 防止组件间紧耦合
- 定义最小片段 - 仅请求必要字段
- 使用参数提高灵活性 - 使片段可重用
- 遵循命名约定 - 组件名称_属性名称模式
- 避免循环依赖 - 仔细设计片段层次结构
- 使用可重新获取的片段 - 启用组件级重新获取
- 处理加载状态 - 在数据获取期间提供反馈
- 正确类型化片段 - 通过TypeScript确保类型安全
常见陷阱
- 片段过度获取 - 请求不必要字段
- 缺少数据掩码 - 访问未声明的字段
- 循环片段引用 - 创建依赖循环
- 不当片段组合 - 未展开子片段
- 硬编码值 - 不使用片段参数
- 破坏数据契约 - 随意更改片段字段
- 缺少片段键 - 在列表中未提供唯一键
- 忽略类型条件 - 未正确处理联合类型
- 过度嵌套 - 创建过深的片段层次结构
- 错误处理不足 - 未优雅处理缺失数据
使用场景
- 使用GraphQL构建React应用程序
- 实现组件驱动架构
- 创建可重用UI组件
- 开发数据密集型应用程序
- 构建社交媒体平台
- 创建电子商务应用程序
- 实现协作工具
- 开发内容管理系统
- 构建管理仪表板
- 使用React Native创建移动应用程序