Relay突变模式Skill relay-mutations-patterns

Relay突变模式是React Relay库中用于管理GraphQL数据更新的核心技术,包括乐观更新、连接处理、声明式突变和全面错误处理,适用于前端开发中构建高效、响应式应用程序。关键词:Relay, GraphQL, 数据突变, 乐观更新, 前端开发, 缓存更新, 错误处理, 连接更新

前端开发 0 次安装 0 次浏览 更新于 3/25/2026

名称: 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; // 此组件仅管理订阅
}

最佳实践

  1. 使用乐观更新 - 提高感知性能
  2. 优雅处理错误 - 在失败时提供用户反馈
  3. 正确更新连接 - 使用@appendEdge/@prependEdge指令
  4. 实施适当验证 - 在提交突变前验证
  5. 删除后清理 - 从缓存中移除已删除项
  6. 使用声明式配置 - 集中突变配置
  7. 批量相关突变 - 减少网络开销
  8. 实施重试逻辑 - 处理临时故障
  9. 跟踪突变状态 - 显示加载指示器
  10. 测试突变更新器 - 确保缓存更新正确

常见陷阱

  1. 缺少更新器 - 突变后未更新缓存
  2. 错误的乐观更新 - 乐观数据与实际情况不匹配
  3. 内存泄漏 - 未处理订阅
  4. 竞态条件 - 多个并发突变
  5. 过时的连接ID - 使用错误的连接标识符
  6. 缺少错误处理 - 未处理突变失败
  7. 过度乐观更新 - 对不安全操作进行乐观更新
  8. 缓存不一致 - 手动更新导致数据不匹配
  9. 缺少回滚 - 未回滚失败的乐观更新
  10. 突变期间用户体验差 - 无加载或错误反馈

何时使用

  • 创建、更新或删除数据
  • 实施用户交互
  • 构建实时协作功能
  • 开发表单提交
  • 创建喜欢/收藏功能
  • 实施评论系统
  • 构建购物车
  • 开发社交功能
  • 创建管理界面
  • 实施批量操作

资源