| name | relay-mutations-patterns |
| user-invocable | false |
| description | Use when relay mutations with optimistic updates, connections, declarative mutations, and error handling. |
| allowed-tools | ["Read","Write","Edit","Grep","Glob","Bash"] |
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
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
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);
}}>
< = = = />
{isInFlight ? 'Creating...' : 'Create Post'}
);
}
2. Optimistic Updates
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, );
}
}
});
};
(
);
}
3. Connection Updates
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]
},
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 (
);
}
() {
data = (
graphql,
post
);
connectionID = .(
post.,
);
(
);
}
4. Manual Cache Updates
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.(connection, postId);
}
},
: {
onDelete?.();
}
});
};
(
);
}
5. Complex Updater Functions
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) {
postsCount = author.() || ;
author.(postsCount, );
}
}
},
: {
: {
: {
: post.,
title,
body,
status,
: ().()
}
}
}
});
};
;
}
6. Multiple Mutations
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');
}
},
: {
draftConnection = store.(draftConnectionID);
(draftConnection) {
.(draftConnection, post.);
}
postRecord = store.(post.);
(postRecord) {
postRecord.(, );
postRecord.( ().(), );
}
}
});
};
(
);
}
7. Error Handling
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) {
setErrors(errors.map(e => e.message));
} else if (response.createPost.errors) {
setErrors(response.createPost.errors);
} else {
console.log('Post created successfully');
}
},
onError: (error) => {
setErrors(['Network error. Please try again.']);
.(, error);
}
});
};
(
);
}
8. Batched Mutations
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 => {
(connection) {
.(connection, id);
}
});
}
});
};
= () => {
({
: {
: { : selectedPostIds }
},
: {
selectedPostIds.( {
postRecord = store.(id);
(postRecord) {
postRecord.(, );
}
});
}
});
};
(
);
}
9. Declarative Mutation Configuration
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'
}];
},
() {
.(, response...);
},
() {
.(, errors);
}
};
() {
[commit] = (createPostConfig.);
= () => {
({
: createPostConfig.(input),
: createPostConfig.(input),
: createPostConfig.(),
: {
(errors) {
createPostConfig.(errors);
} {
createPostConfig.(response);
}
}
});
};
;
}
10. Subscription-Like Mutations
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);
}
}
},
: {
.(, response.);
},
: {
.(, error);
}
});
subscription.();
}, [postId]);
;
}
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