名称: relay-突变模式 用户可调用: false 描述: 当使用Relay突变时,包括乐观更新、连接处理、声明式突变和错误处理。 允许工具:
- 读取
- 写入
- 编辑
- 查找
- 通配符
- Bash
Relay突变模式
掌握Relay突变,用于构建具有乐观更新、连接处理和声明式数据更新的交互式应用程序。
概述
Relay突变提供了一种声明式更新数据的方式,具有自动缓存更新、乐观响应和错误回滚功能。突变无缝集成到Relay的规范化缓存和连接协议中。
安装和设置
突变配置
// mutations/CreatePostMutation.js
import { graphql, commitMutation } from 'react-relay';
import environment from '../RelayEnvironment';
const mutation = graphql`
mutation CreatePostMutation($input: CreatePostInput!) {
createPost(input: $input) {
postEdge {
__typename
cursor
node {
id
title
body
createdAt
author {
id
name
}
}
}
}
}
`;
export default function createPost(title, body) {
return new Promise((resolve, reject) => {
commitMutation(environment, {
mutation,
variables: {
input: { title, body }
},
onCompleted: (response, errors) => {
if (errors) {
reject(errors);
} else {
resolve(response);
}
},
onError: reject
});
});
}
核心模式
1. 基本突变
// CreatePost.jsx
import { graphql, useMutation } from 'react-relay';
const CreatePostMutation = graphql`
mutation CreatePostMutation($input: CreatePostInput!) {
createPost(input: $input) {
post {
id
title
body
author {
id
name
}
}
}
}
`;
function CreatePost() {
const [commit, isInFlight] = useMutation(CreatePostMutation);
const handleSubmit = (title, body) => {
commit({
variables: {
input: { title, body }
},
onCompleted: (response, errors) => {
if (errors) {
console.error('错误:', errors);
} else {
console.log('帖子创建:', response.createPost.post);
}
},
onError: (error) => {
console.error('网络错误:', error);
}
});
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(e.target.title.value, e.target.body.value);
}}>
<input name="title" placeholder="标题" disabled={isInFlight} />
<textarea name="body" placeholder="内容" disabled={isInFlight} />
<button type="submit" disabled={isInFlight}>
{isInFlight ? '创建中...' : '创建帖子'}
</button>
</form>
);
}
2. 乐观更新
// LikeButton.jsx
import { graphql, useMutation } from 'react-relay';
const LikePostMutation = graphql`
mutation LikePostMutation($input: LikePostInput!) {
likePost(input: $input) {
post {
id
likesCount
viewerHasLiked
}
}
}
`;
function LikeButton({ post }) {
const [commit, isInFlight] = useMutation(LikePostMutation);
const handleLike = () => {
commit({
variables: {
input: { postId: post.id }
},
// 乐观响应
optimisticResponse: {
likePost: {
post: {
id: post.id,
likesCount: post.likesCount + 1,
viewerHasLiked: true
}
}
},
// 乐观更新器
optimisticUpdater: (store) => {
const postRecord = store.get(post.id);
if (postRecord) {
postRecord.setValue(post.likesCount + 1, 'likesCount');
postRecord.setValue(true, 'viewerHasLiked');
}
}
});
};
return (
<button onClick={handleLike} disabled={isInFlight}>
{post.viewerHasLiked ? '取消喜欢' : '喜欢'} ({post.likesCount})
</button>
);
}
3. 连接更新
// CreateComment.jsx
const CreateCommentMutation = graphql`
mutation CreateCommentMutation(
$input: CreateCommentInput!
$connections: [ID!]!
) {
createComment(input: $input) {
commentEdge @appendEdge(connections: $connections) {
cursor
node {
id
body
createdAt
author {
id
name
avatar
}
}
}
}
}
`;
function CreateComment({ postId, connectionID }) {
const [commit, isInFlight] = useMutation(CreateCommentMutation);
const handleSubmit = (body) => {
commit({
variables: {
input: { postId, body },
connections: [connectionID]
},
// 不需要手动更新器,@appendEdge处理
optimisticResponse: {
createComment: {
commentEdge: {
cursor: '临时游标',
node: {
id: `temp-${Date.now()}`,
body,
createdAt: new Date().toISOString(),
author: {
id: currentUser.id,
name: currentUser.name,
avatar: currentUser.avatar
}
}
}
}
}
});
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(e.target.body.value);
e.target.reset();
}}>
<textarea name="body" placeholder="添加评论..." />
<button type="submit" disabled={isInFlight}>发布</button>
</form>
);
}
// 使用连接ID
function Post({ post }) {
const data = useFragment(
graphql`
fragment Post_post on Post {
id
comments(first: 10)
@connection(key: "Post_comments") {
edges {
node {
id
...Comment_comment
}
}
}
}
`,
post
);
const connectionID = ConnectionHandler.getConnectionID(
post.id,
'Post_comments'
);
return (
<div>
<CommentsList comments={data.comments.edges} />
<CreateComment postId={post.id} connectionID={connectionID} />
</div>
);
}
4. 手动缓存更新
// DeletePost.jsx
const DeletePostMutation = graphql`
mutation DeletePostMutation($input: DeletePostInput!) {
deletePost(input: $input) {
deletedPostId
}
}
`;
function DeletePost({ postId, onDelete }) {
const [commit] = useMutation(DeletePostMutation);
const handleDelete = () => {
commit({
variables: {
input: { id: postId }
},
updater: (store) => {
// 从连接中移除
const root = store.getRoot();
const connection = ConnectionHandler.getConnection(
root,
'PostsList_posts'
);
if (connection) {
ConnectionHandler.deleteNode(connection, postId);
}
// 删除记录
store.delete(postId);
},
optimisticUpdater: (store) => {
const root = store.getRoot();
const connection = ConnectionHandler.getConnection(
root,
'PostsList_posts'
);
if (connection) {
ConnectionHandler.deleteNode(connection, postId);
}
},
onCompleted: () => {
onDelete?.();
}
});
};
return (
<button onClick={handleDelete} className="delete-button">
删除
</button>
);
}
5. 复杂更新器函数
// UpdatePost.jsx
const UpdatePostMutation = graphql`
mutation UpdatePostMutation($input: UpdatePostInput!) {
updatePost(input: $input) {
post {
id
title
body
status
updatedAt
}
}
}
`;
function UpdatePost({ post }) {
const [commit] = useMutation(UpdatePostMutation);
const handleUpdate = (title, body, status) => {
commit({
variables: {
input: {
id: post.id,
title,
body,
status
}
},
updater: (store, data) => {
const updatedPost = data.updatePost.post;
const postRecord = store.get(updatedPost.id);
if (postRecord) {
postRecord.setValue(updatedPost.title, 'title');
postRecord.setValue(updatedPost.body, 'body');
postRecord.setValue(updatedPost.status, 'status');
postRecord.setValue(updatedPost.updatedAt, 'updatedAt');
// 更新相关记录
const author = postRecord.getLinkedRecord('author');
if (author) {
const postsCount = author.getValue('postsCount') || 0;
author.setValue(postsCount, 'postsCount');
}
}
},
optimisticResponse: {
updatePost: {
post: {
id: post.id,
title,
body,
status,
updatedAt: new Date().toISOString()
}
}
}
});
};
return <EditForm post={post} onSubmit={handleUpdate} />;
}
6. 多个突变
// PublishPost.jsx
const PublishPostMutation = graphql`
mutation PublishPostMutation($input: PublishPostInput!) {
publishPost(input: $input) {
post {
id
status
publishedAt
}
edge @prependEdge(connections: $connections) {
cursor
node {
id
...PostCard_post
}
}
}
}
`;
function PublishPost({ post, draftConnectionID, publishedConnectionID }) {
const [commit] = useMutation(PublishPostMutation);
const handlePublish = () => {
commit({
variables: {
input: { id: post.id },
connections: [publishedConnectionID]
},
updater: (store) => {
// 从草稿中移除
const draftConnection = store.get(draftConnectionID);
if (draftConnection) {
ConnectionHandler.deleteNode(draftConnection, post.id);
}
// 更新帖子状态
const postRecord = store.get(post.id);
if (postRecord) {
postRecord.setValue('PUBLISHED', 'status');
postRecord.setValue(new Date().toISOString(), 'publishedAt');
}
},
optimisticUpdater: (store) => {
const draftConnection = store.get(draftConnectionID);
if (draftConnection) {
ConnectionHandler.deleteNode(draftConnection, post.id);
}
const postRecord = store.get(post.id);
if (postRecord) {
postRecord.setValue('PUBLISHED', 'status');
postRecord.setValue(new Date().toISOString(), 'publishedAt');
}
}
});
};
return (
<button onClick={handlePublish}>
发布
</button>
);
}
7. 错误处理
// CreatePostWithValidation.jsx
function CreatePostWithValidation() {
const [commit, isInFlight] = useMutation(CreatePostMutation);
const [errors, setErrors] = useState(null);
const handleSubmit = (title, body) => {
setErrors(null);
commit({
variables: {
input: { title, body }
},
onCompleted: (response, errors) => {
if (errors) {
// GraphQL错误
setErrors(errors.map(e => e.message));
} else if (response.createPost.errors) {
// 应用程序错误
setErrors(response.createPost.errors);
} else {
// 成功
console.log('帖子创建成功');
}
},
onError: (error) => {
// 网络或运行时错误
setErrors(['网络错误,请重试。']);
console.error('突变错误:', error);
}
});
};
return (
<div>
{errors && (
<div className="error-list">
{errors.map((error, i) => (
<div key={i} className="error">{error}</div>
))}
</div>
)}
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit(
e.target.title.value,
e.target.body.value
);
}}>
<input name="title" required disabled={isInFlight} />
<textarea name="body" required disabled={isInFlight} />
<button type="submit" disabled={isInFlight}>
创建帖子
</button>
</form>
</div>
);
}
8. 批量突变
// BulkActions.jsx
function BulkActions({ selectedPostIds }) {
const [deletePosts] = useMutation(DeletePostsMutation);
const [archivePosts] = useMutation(ArchivePostsMutation);
const handleBulkDelete = () => {
deletePosts({
variables: {
input: { ids: selectedPostIds }
},
updater: (store) => {
const root = store.getRoot();
const connection = ConnectionHandler.getConnection(
root,
'PostsList_posts'
);
selectedPostIds.forEach(id => {
if (connection) {
ConnectionHandler.deleteNode(connection, id);
}
store.delete(id);
});
},
optimisticUpdater: (store) => {
const root = store.getRoot();
const connection = ConnectionHandler.getConnection(
root,
'PostsList_posts'
);
selectedPostIds.forEach(id => {
if (connection) {
ConnectionHandler.deleteNode(connection, id);
}
});
}
});
};
const handleBulkArchive = () => {
archivePosts({
variables: {
input: { ids: selectedPostIds }
},
updater: (store) => {
selectedPostIds.forEach(id => {
const postRecord = store.get(id);
if (postRecord) {
postRecord.setValue('ARCHIVED', 'status');
}
});
}
});
};
return (
<div>
<button onClick={handleBulkDelete}>删除选中</button>
<button onClick={handleBulkArchive}>归档选中</button>
</div>
);
}
9. 声明式突变配置
// mutations/configs.js
import { ConnectionHandler } from 'relay-runtime';
export const createPostConfig = {
mutation: CreatePostMutation,
getVariables(input) {
return { input };
},
getOptimisticResponse(input) {
return {
createPost: {
postEdge: {
node: {
id: `temp-${Date.now()}`,
title: input.title,
body: input.body,
createdAt: new Date().toISOString(),
author: {
id: currentUser.id,
name: currentUser.name
}
}
}
}
};
},
getConfigs() {
return [{
type: 'RANGE_ADD',
parentID: 'client:root',
connectionInfo: [{
key: 'PostsList_posts',
rangeBehavior: 'prepend'
}],
edgeName: 'postEdge'
}];
},
onSuccess(response) {
console.log('帖子创建:', response.createPost.postEdge.node);
},
onFailure(errors) {
console.error('创建帖子失败:', errors);
}
};
// 使用
function CreatePost() {
const [commit] = useMutation(createPostConfig.mutation);
const handleSubmit = (input) => {
commit({
variables: createPostConfig.getVariables(input),
optimisticResponse: createPostConfig.getOptimisticResponse(input),
configs: createPostConfig.getConfigs(),
onCompleted: (response, errors) => {
if (errors) {
createPostConfig.onFailure(errors);
} else {
createPostConfig.onSuccess(response);
}
}
});
};
return <CreatePostForm onSubmit={handleSubmit} />;
}
10. 订阅式突变
// RealtimeComments.jsx
import { requestSubscription, graphql } from 'react-relay';
const CommentAddedSubscription = graphql`
subscription CommentAddedSubscription($postId: ID!) {
commentAdded(postId: $postId) {
commentEdge {
cursor
node {
id
body
createdAt
author {
id
name
}
}
}
}
}
`;
function RealtimeComments({ postId }) {
useEffect(() => {
const subscription = requestSubscription(environment, {
subscription: CommentAddedSubscription,
variables: { postId },
updater: (store) => {
const payload = store.getRootField('commentAdded');
const edge = payload.getLinkedRecord('commentEdge');
const node = edge.getLinkedRecord('node');
// 添加到连接
const post = store.get(postId);
if (post) {
const connection = ConnectionHandler.getConnection(
post,
'Post_comments'
);
if (connection) {
ConnectionHandler.insertEdgeAfter(connection, edge);
}
}
},
onNext: (response) => {
console.log('新评论:', response.commentAdded);
},
onError: (error) => {
console.error('订阅错误:', error);
}
});
return () => subscription.dispose();
}, [postId]);
return null; // 此组件仅管理订阅
}
最佳实践
- 使用乐观更新 - 提高感知性能
- 优雅处理错误 - 在失败时提供用户反馈
- 正确更新连接 - 使用@appendEdge/@prependEdge指令
- 实施适当验证 - 在提交突变前验证
- 删除后清理 - 从缓存中移除已删除项
- 使用声明式配置 - 集中突变配置
- 批量相关突变 - 减少网络开销
- 实施重试逻辑 - 处理临时故障
- 跟踪突变状态 - 显示加载指示器
- 测试突变更新器 - 确保缓存更新正确
常见陷阱
- 缺少更新器 - 突变后未更新缓存
- 错误的乐观更新 - 乐观数据与实际情况不匹配
- 内存泄漏 - 未处理订阅
- 竞态条件 - 多个并发突变
- 过时的连接ID - 使用错误的连接标识符
- 缺少错误处理 - 未处理突变失败
- 过度乐观更新 - 对不安全操作进行乐观更新
- 缓存不一致 - 手动更新导致数据不匹配
- 缺少回滚 - 未回滚失败的乐观更新
- 突变期间用户体验差 - 无加载或错误反馈
何时使用
- 创建、更新或删除数据
- 实施用户交互
- 构建实时协作功能
- 开发表单提交
- 创建喜欢/收藏功能
- 实施评论系统
- 构建购物车
- 开发社交功能
- 创建管理界面
- 实施批量操作