relay-mutations-patterns

Relay Mutations Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "relay-mutations-patterns" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-relay-mutations-patterns

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

  1. 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> ); }

  1. 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> ); }

  1. 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> ); }

  1. 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} />; }

  1. 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> ); }

  1. 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> )}

  &#x3C;form onSubmit={(e) => {
    e.preventDefault();
    handleSubmit(
      e.target.title.value,
      e.target.body.value
    );
  }}>
    &#x3C;input name="title" required disabled={isInFlight} />
    &#x3C;textarea name="body" required disabled={isInFlight} />
    &#x3C;button type="submit" disabled={isInFlight}>
      Create Post
    &#x3C;/button>
  &#x3C;/form>
&#x3C;/div>

); }

  1. 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> ); }

  1. 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} />; }

  1. 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

  • Use optimistic updates - Improve perceived performance

  • Handle errors gracefully - Provide user feedback on failures

  • Update connections properly - Use @appendEdge/@prependEdge directives

  • Implement proper validation - Validate before committing mutations

  • Clean up after deletions - Remove deleted items from cache

  • Use declarative configs - Centralize mutation configuration

  • Batch related mutations - Reduce network overhead

  • Implement retry logic - Handle transient failures

  • Track mutation state - Show loading indicators

  • Test mutation updaters - Ensure cache updates work correctly

Common Pitfalls

  • Missing updaters - Not updating cache after mutations

  • Incorrect optimistic updates - Optimistic data doesn't match reality

  • Memory leaks - Not disposing subscriptions

  • Race conditions - Multiple concurrent mutations

  • Stale connection IDs - Using wrong connection identifiers

  • Missing error handling - Not handling mutation failures

  • Over-optimistic updates - Optimistic updates for unsafe operations

  • Cache inconsistencies - Manual updates causing data mismatches

  • Missing rollbacks - Not reverting failed optimistic updates

  • 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

  • Relay Mutations Guide

  • Relay Updater Functions

  • Optimistic Updates

  • Connection Handler

  • Relay Examples

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

atomic-design-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review