Support Ukraine 🇺🇦Help Ukrainian ArmyHumanitarian Assistance to Ukrainians

How to update a connection after a mutation in Relay?

Travis

Jan 27 2021 at 18:41 GMT

Let's say that I have a Relay app in which I show a movie with its reviews. The reviews are paginated using a connection. Here's how the movie fragment looks like:

fragment Movie_movie on Movie {
  title
  imageUrl
  reviews(first: $count, after: $cursor) @connection(key: "Movie_movie_reviews") {
    edges {
      node {
        id
        rating
        text
      }
    }
  }
}

The user can leave a review on the movie with the following mutation:

mutation AddReviewMutation($input: AddReviewInput!) {
  addReview(input: $input) {
    review {
      id
      rating
      text
    }
  }
}

After the mutation is executed, I would like to update the reviews connection by adding to it the newly created review so that the user can see their new review. How can I do that in Relay?

Also, if the user deletes their review, how do I remove the edge with that review from the connection? Here's the mutation to delete a review:

mutation DeleteReviewMutation($input: DeleteReviewInput!) {
  deleteReview(input: $input) {
    deletedReviewId
  }
}

1 Answer

thoughtful

Jan 27 2021 at 22:33 GMT

A common way to update a connection after a mutation is by using an updater function, which I'll cover first.

A newer and more concise way to update a connection is to use the @appendNode, @prependNode, and @deleteEdge directives, which I'll cover in the end.

Updating the connection with an updater function

You can update the reviews connection by specifying an updater function for your mutation:

commitMutation(
  relayEnvironment,
  {
    mutation,
    variables,
    updater
  }
);

I'll now explain how to append or prepend a new review to the connection as well as how to delete a review from the connection.

Append a review to the connection

Let's see how to insert a review at the end of the connection.

We start by writing the updater function, which takes the Relay store as argument:

const updater = (store) => {

};

The first thing we need to do is to access the payload of the addReview mutation:

const addReviewPayload = store.getRootField('addReview');

If there's no payload, then we have nothing to update:

if (!addReviewPayload) {
  return;
}

From the payload, we can access the new review:

const newReview = addReviewPayload.getLinkedRecord('review');

Next, let's get the movie to which the review was added. Since we know the movie ID, we can use store.get:

const movie = store.get(movieId);

We can then access the reviews connection by passing the movie record and the connection key to ConnectionHandler.getConnection:

const reviewsConnection = ConnectionHandler.getConnection(
  movie,
  'Movie_movie_reviews'
);

Now, let's create an edge that will contain the new review. We can do so by passing the store, the connection to which the edge will be added, the node of the edge, and the GraphQL type name of the edge to ConnectionHandler.createEdge:

const newReviewEdge = ConnectionHandler.createEdge(
  store,
  reviewsConnection,
  newReview,
  'ReviewEdge'
);

Finally, we can append the edge to the connection:

ConnectionHandler.insertEdgeAfter(
  reviewsConnection,
  newReviewEdge
);

That's it! This is how the final updater function looks like:

const updater = (store) => {
  const addReviewPayload = store.getRootField('addReview');
  
  if (!addReviewPayload) {
    return;
  }

  const newReview = addReviewPayload.getLinkedRecord('review');
  const movie = store.get(movieId);
  const reviewsConnection = ConnectionHandler.getConnection(
    movie,
    'Movie_movie_reviews'
  );
  const newReviewEdge = ConnectionHandler.createEdge(
    store,
    reviewsConnection,
    newReview,
    'ReviewEdge'
  );
  
  ConnectionHandler.insertEdgeAfter(
    reviewsConnection,
    newReviewEdge
  );
};

If you're not comfortable working with the Relay store, I recommend to check out What is the Relay store and how to access/update the data in it? (In-depth explanation).

Prepend a review to the connection

Let's see how to insert a review at the beginning of the connection.

The updater function is the same as the one for appending, the only difference is that we use ConnectionHandler.insertEdgeBefore instead of ConnectionHandler.insertEdgeAfter.

Delete a review from the connection

Let's see how to delete a review from the connection.

The updater function is very similar to the one we saw, the main difference is that instead of creating a new edge and adding it to the connection, we delete the edge that contains the node with the specified ID.

Again, we start by writing the updater function in which we first access the mutation payload:

const updater = (store) => {
  const deleteReviewPayload = store.getRootField('deleteReview');
  
  if (!deleteReviewPayload) {
    return;
  }
  
  // Continues
};

From the payload, we access the ID of the deleted review (this time using getValue instead of getLinkedRecord since the deletedReviewId field is just a scalar):

const deletedReviewId = deleteReviewPayload.getValue('deletedReviewId');

Next, we access the movie and the reviews connection:

const movie = store.get(movieId);
const reviewsConnection = ConnectionHandler.getConnection(
  movie,
  'Movie_movie_reviews'
);

Finally, we delete the review from the connection by passing the connection and the ID of the review to ConnectionHandler.deleteNode:

ConnectionHandler.deleteNode(
  reviewsConnection,
  deletedReviewId
);

This will find the edge that contains the node with the given ID and remove that edge from the connection.

That's it! Here's how the final updater function looks like:

const updater = (store) => {
  const deleteReviewPayload = store.getRootField('deleteReview');
  
  if (!deleteReviewPayload) {
    return;
  }

  const deletedReviewId = deleteReviewPayload.getValue('deletedReviewId');
  const movie = store.get(movieId);
  const reviewsConnection = ConnectionHandler.getConnection(
    movie,
    'Movie_movie_reviews'
  );
  
  ConnectionHandler.deleteNode(
    reviewsConnection,
    deletedReviewId
  );
};

Updating the connection using the @appendNode, @prependNode, and @deleteEdge directives

Let's see how we can achieve the same that we did above but in a more concise and declarative way by using Relay directives.

Append a review with @appendNode

To append a review to the connection with @appendNode, we need to add the @appendNode directive on the review field of the mutation payload.

The @appendNode directive takes the list of connection IDs to which to append the node (in our case it's just one connection) and the name of the GraphQL type for the edge that will contain the node (in our case, it's simply "ReviewEdge").

We can pass the connection IDs as a variable while specifying the edge type name directly inline.

Here's how the modified AddReviewMutation will look like:

mutation AddReviewMutation($input: AddReviewInput!, $connections: [ID!]!) {
  addReview(input: $input) {
    review @appendNode(connections: $connections, edgeTypeName: "ReviewEdge") {
      id
      rating
      text
    }
  }
}

In order to pass the ID of the reviews connection as a variable to our mutation, we need to query the __id field on that connection:

fragment Movie_movie on Movie {
  reviews(first: $count, after: $cursor) @connection(key: "Movie_movie_reviews") {
    __id
    edges {
      ...
    }
  }
}

Then, we can define the variables that we pass to our mutation like this:

const variables = {
  input: {...},
  connections: movie.reviews ? [movie.reviews.__id] : []
};

The reason why we use the conditional operator is that the movie.reviews connection is nullable, so we need this extra check.

That's it!

Prepend a review with @prependNode

The process of prepending a review to the connection with @prependNode is exactly the same as we saw for @appendNode, the only difference is that we specify the @prependNode directive on the review field instead of @appendNode.

Delete a review with @deleteEdge

To delete a review from the connection with @deleteEdge, we need to add the @deleteEdge directive on the deletedReviewId field of the mutation payload.

The @deleteEdge directive takes the list of connection IDs from which to delete the edge whose node has an ID equal to the one received in the mutation payload.

Here's how the modified DeleteReviewMutation will look like:

mutation DeleteReviewMutation($input: DeleteReviewInput!, $connections: [ID!]!) {
  deleteReview(input: $input) {
    deletedReviewId @deleteEdge(connections: $connections)
  }
}

Again, to get the ID of the connection from which we want to remove the review, we need to query the __id field on that connection. Then, we can just access movie.reviews.__id and pass it to the mutation:

const variables = {
  input: {...},
  connections: movie.reviews ? [movie.reviews.__id] : []
};

Conclusion

You should now have a good understanding on how to update a connection after a mutation both with an updater function as well as with Relay directives.

claritician © 2022