Token导航 LogoToken导航TokenDH.com
开发需要联网github未标认证来源可访问clear审计提醒

relay-mutations-patterns中继突变模式

Agent Skill

relay-mutations-patterns 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息,适合在 Codex、Claude、Cursor、Gemini CLI 中需要围绕仓库状态、代码变更或协作事项进行整理时使用。可结合来源仓库、安装命令和原始 README 继续核验具体用法。安装前建议确认权限范围、维护状态,以及是否会触发联网、命令执行或文件读写。

总安装

774

周安装

31

GitHub Stars

142

下载量

250
CodexClaudeCursorGemini CLI

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

GitHub

来源数

3

许可证

MIT

最后核验

2026-05-01

来源状态

来源可访问

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:relay-mutations-patterns(中继突变模式)
来源仓库:https://github.com/thebushidocollective/han
仓库路径:skills/relay-mutations-patterns
安装命令:
npx skills add https://github.com/thebushidocollective/han --skill relay-mutations-patterns
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。不同来源提供的安装方式可能略有差异;本站展示可直接复制的安装命令,安装前请核对来源页面。

skills.shnpx skills
npx skills add https://github.com/thebushidocollective/han --skill relay-mutations-patterns

简介

relay-mutations-patterns 用于处理 GitHub 仓库、Issue、Pull Request 和代码协作信息。

  • 适合在 Codex、Claude、Cursor、Gemini CLI 中围绕仓库状态、代码变更或协作事项进行整理。
  • 通过 npx skills add 命令从指定 GitHub 仓库安装并使用。
  • 安装前需确认权限范围、维护状态,注意是否触发联网、命令执行或文件读写操作。
  • 适用宿主包括 Codex、Claude、Cursor、Gemini CLI,接入前应确认版本、权限和运行环境要求。

SKILL.md

Relay Mutations Patterns

Master Relay mutations for building interactive applications with optimistic updates, connection handling, and declarative data updates.

Overview

Relay mutations provide a declarative way to update data with automatic cache updates, optimistic responses, and rollback on error. Mutations integrate seamlessly with Relay's normalized cache and connection protocol.

Installation and Setup

Mutation Configuration

// 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
    });
  });
}

Core Patterns

1. Basic Mutation

// 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:', errors);
        } else {
          console.log('Post created:', response.createPost.post);
        }
      },
      onError: (error) => {
        console.error('Network error:', error);
      }
    });
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      handleSubmit(e.target.title.value, e.target.body.value);
    }}>
      <input name="title" placeholder="Title" disabled={isInFlight} />
      <textarea name="body" placeholder="Body" disabled={isInFlight} />
      <button type="submit" disabled={isInFlight}>
        {isInFlight ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

2. Optimistic Updates

// 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 }
      },

      // Optimistic response
      optimisticResponse: {
        likePost: {
          post: {
            id: post.id,
            likesCount: post.likesCount + 1,
            viewerHasLiked: true
          }
        }
      },

      // Optimistic updater
      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 ? 'Unlike' : 'Like'} ({post.likesCount})
    </button>
  );
}

3. Connection Updates

// 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]
      },

      // No manual updater needed, @appendEdge handles it

      optimisticResponse: {
        createComment: {
          commentEdge: {
            cursor: 'temp-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="Add a comment..." />
      <button type="submit" disabled={isInFlight}>Post</button>
    </form>
  );
}

// Usage with connection 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. Manual Cache Updates

// 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) => {
        // Remove from connection
        const root = store.getRoot();
        const connection = ConnectionHandler.getConnection(
          root,
          'PostsList_posts'
        );

        if (connection) {
          ConnectionHandler.deleteNode(connection, postId);
        }

        // Delete the record
        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">
      Delete
    </button>
  );
}

5. Complex Updater Functions

// 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');

          // Update related records
          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. Multiple Mutations

// 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) => {
        // Remove from drafts
        const draftConnection = store.get(draftConnectionID);
        if (draftConnection) {
          ConnectionHandler.deleteNode(draftConnection, post.id);
        }

        // Update post status
        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}>
      Publish
    </button>
  );
}

7. Error Handling

// 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 errors
          setErrors(errors.map(e => e.message));
        } else if (response.createPost.errors) {
          // Application errors
          setErrors(response.createPost.errors);
        } else {
          // Success
          console.log('Post created successfully');
        }
      },

      onError: (error) => {
        // Network or runtime errors
        setErrors(['Network error. Please try again.']);
        console.error('Mutation 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}>
          Create Post
        </button>
      </form>
    </div>
  );
}

8. Batched Mutations

// 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}>Delete Selected</button>
      <button onClick={handleBulkArchive}>Archive Selected</button>
    </div>
  );
}

9. Declarative Mutation Configuration

// 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('Post created:', response.createPost.postEdge.node);
  },

  onFailure(errors) {
    console.error('Failed to create post:', errors);
  }
};

// Usage
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. Subscription-Like Mutations

// 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');

        // Add to connection
        const post = store.get(postId);
        if (post) {
          const connection = ConnectionHandler.getConnection(
            post,
            'Post_comments'
          );

          if (connection) {
            ConnectionHandler.insertEdgeAfter(connection, edge);
          }
        }
      },

      onNext: (response) => {
        console.log('New comment:', response.commentAdded);
      },

      onError: (error) => {
        console.error('Subscription error:', error);
      }
    });

    return () => subscription.dispose();
  }, [postId]);

  return null; // This component just manages the subscription
}

Best Practices

  1. Use optimistic updates - Improve perceived performance
  2. Handle errors gracefully - Provide user feedback on failures
  3. Update connections properly - Use @appendEdge/@prependEdge directives
  4. Implement proper validation - Validate before committing mutations
  5. Clean up after deletions - Remove deleted items from cache
  6. Use declarative configs - Centralize mutation configuration
  7. Batch related mutations - Reduce network overhead
  8. Implement retry logic - Handle transient failures
  9. Track mutation state - Show loading indicators
  10. Test mutation updaters - Ensure cache updates work correctly

Common Pitfalls

  1. Missing updaters - Not updating cache after mutations
  2. Incorrect optimistic updates - Optimistic data doesn't match reality
  3. Memory leaks - Not disposing subscriptions
  4. Race conditions - Multiple concurrent mutations
  5. Stale connection IDs - Using wrong connection identifiers
  6. Missing error handling - Not handling mutation failures
  7. Over-optimistic updates - Optimistic updates for unsafe operations
  8. Cache inconsistencies - Manual updates causing data mismatches
  9. Missing rollbacks - Not reverting failed optimistic updates
  10. Poor UX during mutations - No loading or error feedback

When to Use

  • Creating, updating, or deleting data
  • Implementing user interactions
  • Building real-time collaborative features
  • Developing form submissions
  • Creating like/favorite functionality
  • Implementing comment systems
  • Building shopping carts
  • Developing social features
  • Creating admin interfaces
  • Implementing bulk operations

Resources

适合场景

01

用户想查找某类 Agent Skill 时

02

需要根据任务场景推荐可安装能力包时

03

需要对比不同来源的安装命令和来源信息时

04

需要参考平台分布和安装热度时

能力概览

能力 1

按任务关键词查找相关 Skills

能力 2

展示可复制的安装命令

能力 3

保留来源站点、仓库和原始说明,方便继续核验

能力 4

补充不同宿主或平台的使用分布数据

能力 5

展示第三方安全扫描或审计结果

安装后应在对应宿主中按原始 README 的触发条件使用;具体调用方式请以来源页面和 README 为准。

平台分布

OpenCode

30.27%
按下载量换算76

Claude Code

21.27%
按下载量换算53

Codex

18.75%
按下载量换算47

windsurf

12.44%
按下载量换算31

Antigravity

7.33%
按下载量换算18

Cursor

3.04%
按下载量换算8

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

可疑

权限和风险

需要联网

该 Skill 可能需要联网访问来源站点、仓库或外部 API;具体网络访问范围需要结合源码和 README 复核。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。来源安全扫描存在 warning/failed 结果,不能写成本站确认安全。

来源信息

继续浏览同类 Skills