Launch Apollo Studio

Mutations in Apollo Client

Modify data with the useMutation hook


Now that we've learned how to query data from our backend with Apollo Client, the natural next step is to learn how to modify back-end data with mutations.

This article demonstrates how to send updates to your GraphQL server with the useMutation hook. You'll also learn how to update the Apollo Client cache after executing a mutation, and how to track loading and error states.

To follow along with the examples below, open up our starter project and sample GraphQL server on CodeSandbox. You can view the completed version of the app here.

Prerequisites

This article assumes you're familiar with building basic GraphQL mutations. If you need a refresher, we recommend that you read this guide.

This article also assumes that you've already set up Apollo Client and have wrapped your React app in an ApolloProvider component. For help with those steps, get started.

Executing a mutation

The useMutation React hook is the primary API for executing mutations in an Apollo application.

To execute a mutation, you first call useMutation within a React component and pass it the mutation you want to execute, like so:

my-component.jsx
import { gql, useMutation } from '@apollo/client';

// Define mutation
const INCREMENT_COUNTER = gql`
  # Increments a back-end counter and gets its resulting value
  mutation IncrementCounter {
    currentValue
  }
`;

function MyComponent() {
  // Pass mutation to useMutation
  const [mutateFunction, { data, loading, error }] = useMutation(INCREMENT_COUNTER);}

As shown above, you use the gql function to parse the mutation string into a GraphQL document that you then pass to useMutation.

When your component renders, useMutation returns a tuple that includes:

  • A mutate function that you can call at any time to execute the mutation
    • Unlike useQuery, useMutation doesn't execute its operation automatically on render. Instead, you call this mutate function.
  • An object with fields that represent the current status of the mutation's execution (data, loading, etc.)
    • This object is similar to the object returned by the useQuery hook. For details, see Result.

Example

Let's say we're creating a to-do list application and we want the user to be able to add items to their list. First, we'll create a corresponding GraphQL mutation named ADD_TODO. Remember to wrap GraphQL strings in the gql function to parse them into query documents:

add-todo.jsx
import { gql, useMutation } from '@apollo/client';

const ADD_TODO = gql`
  mutation AddTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
    }
  }
`;

Next, we'll create a component named AddTodo that represents the submission form for the to-do list. Inside it, we'll pass our ADD_TODO mutation to the useMutation hook:

add-todo.jsx
function AddTodo() {
  let input;
  const [addTodo, { data, loading, error }] = useMutation(ADD_TODO);
  if (loading) return 'Submitting...';
  if (error) return `Submission error! ${error.message}`;

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          addTodo({ variables: { text: input.value } });          input.value = '';
        }}
      >
        <input
          ref={node => {
            input = node;
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}

In this example, our form's onSubmit handler calls the mutate function (named addTodo) that's returned by the useMutation hook. This tells Apollo Client to execute the mutation by sending it to our GraphQL server.

Note that this behavior differs from useQuery, which executes its operation as soon as its component renders. This is because mutations are more commonly executed in response to a user action (such as submitting a form in this case).

Providing options

The useMutation hook accepts an options object as its second parameter. Here's an example that provides some default values for GraphQL variables:

const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
  variables: {
    text: "placeholder",
    someOtherVariable: 1234,
  },
});

All supported options are listed in Options.

You can also provide options directly to your mutate function, as demonstrated in this snippet from the example above:

addTodo({
  variables: {
    text: input.value,
  },
});

Here, we use the variables option to provide the values of any GraphQL variables that our mutation requires (specifically, the text of the created to-do item).

Option precedence

If you provide the same option to both useMutation and your mutate function, the mutate function's value takes precedence. In the specific case of the variables option, the two objects are merged shallowly, which means any variables provided only to useMutation are preserved in the resulting object. This helps you set default values for variables.

In the example snippets above, input.value would override "placeholder" as the value of the text variable. The value of someOtherVariable (1234) would be preserved.

Tracking mutation status

In addition to a mutate function, the useMutation hook returns an object that represents the current state of the mutation's execution. The fields of this object (listed in Result) include booleans that indicate whether the mutate function has been called yet, and whether the mutation's result is currently loading.

The example above destructures the loading and error fields from this object to render the AddTodo component differently depending on the mutation's current status:

if (loading) return 'Submitting...';
if (error) return `Submission error! ${error.message}`;

The useMutation hook also supports onCompleted and onError options if you prefer to use callbacks. See the API reference.

Resetting mutation status

The mutation result object returned by useMutation includes a reset function:

const [login, { data, loading, error, reset }] = useMutation(LOGIN_MUTATION);

Call reset to reset the mutation's result to its initial state (i.e., before the mutate function was called). You can use this to enable users to dismiss mutation result data or errors in the UI.

Calling reset does not remove any cached data returned by the mutation's execution. It only affects the state associated with the useMutation hook, causing the corresponding component to rerender.

function LoginPage () {
  const [login, { error, reset }] = useMutation(LOGIN_MUTATION);
  return (
    <>
      <form>
        <input class="login"/>
        <input class="password"/>
        <button onclick={login}>Login</button>
      </form>
      {
        error &&
        <LoginFailedMessageWindow
          message={error.message}
          onDismiss={() => reset()}        />
      }
    </>
  );
}

Updating local data

When you execute a mutation, you modify back-end data. Usually, you then want to update your locally cached data to reflect the back-end modification. For example, if you execute a mutation to add an item to your to-do list, you also want that item to appear in your cached copy of the list.

Supported methods

The most straightforward way to update your local data is to refetch any queries that might be affected by the mutation. However, this method requires additional network requests.

If your mutation returns all of the objects and fields that it modified, you can update your cache directly without making any followup network requests. However, this method increases in complexity as your mutations become more complex.

If you're just getting started with Apollo Client, we recommend refetching queries to update your cached data. After you get that working, you can improve your app's responsiveness by updating the cache directly.

Refetching queries

If you know that your app usually needs to refetch certain queries after a particular mutation, you can include a refetchQueries array in that mutation's options:

// Refetches two queries after mutation completes
const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
  refetchQueries: [    GET_POST, // DocumentNode object parsed with gql    'GetComments' // Query name  ],});

Each element in the refetchQueries array is one of the following:

  • A DocumentNode object parsed with the gql function
  • The name of a query you've previously executed, as a string (e.g., GetComments)
    • To refer to queries by name, make sure each of your app's queries has a unique name.

Each included query is executed with its most recently provided set of variables.

You can provide the refetchQueries option either to useMutation or to the mutate function. For details, see Option precedence.

Note that in an app with tens or hundreds of different queries, it can be challenging to determine exactly which queries to refetch after a particular mutation.

Updating the cache directly

Include modified objects in mutation responses

In most cases, a mutation response should include any object(s) the mutation modified. This enables Apollo Client to normalize those objects and cache them according to their __typename and id fields (by default).

In the example above, our ADD_TODO mutation might return a Todo object with the following structure:

{
  "__typename": "Todo",
  "id": "5",
  "text": "Buy grapes 🍇"
}

Apollo Client automatically adds the __typename field to every object in your queries and mutations by default.

Upon receiving this response object, Apollo Client caches it with key Todo:5. If a cached object already exists with this key, Apollo Client overwrites any existing fields that are also included in the mutation response (other existing fields are preserved).

Returning modified objects like this is a helpful first step to keeping your cache in sync with your backend. However, it isn't always sufficient. For example, a newly cached object isn't automatically added to any list fields that should now include that object. To accomplish this, you can define an update function.

The update function

When a mutation's response is insufficient to update all modified fields in your cache (such as certain list fields), you can define an update function to apply manual changes to your cached data after a mutation.

You provide an update function to useMutation, like so:

const GET_TODOS = gql`
  query GetTodos {
    todos {
      id
    }
  }
`;

function AddTodo() {
  let input;
  const [addTodo] = useMutation(ADD_TODO, {
    update(cache, { data: { addTodo } }) {      cache.modify({        fields: {          todos(existingTodos = []) {            const newTodoRef = cache.writeFragment({              data: addTodo,              fragment: gql`                fragment NewTodo on Todo {                  id                  type                }              `            });            return [...existingTodos, newTodoRef];          }        }      });    }  });

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          addTodo({ variables: { type: input.value } });
          input.value = "";
        }}
      >
        <input
          ref={node => {
            input = node;
          }}
        />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
}

As shown, the update function is passed a cache object that represents the Apollo Client cache. This object provides access to cache API methods like readQuery/writeQuery, readFragment/writeFragment, modify, and evict. These methods enable you to execute GraphQL operations on the cache as though you're interacting with a GraphQL server.

Learn more about supported cache functions in Interacting with cached data.

The update function is also passed an object with a data property that contains the result of the mutation. You can use this value to update the cache with cache.writeQuery, cache.writeFragment, or cache.modify.

If your mutation specifies an optimistic response, your update function is called twice: once with the optimistic result, and again with the actual result of the mutation when it returns.

When the ADD_TODO mutation executes in the above example, the newly added and returned addTodo object is automatically saved into the cache before the update function runs. However, the cached list of ROOT_QUERY.todos (which is watched by the GET_TODOS query) is not automatically updated. This means that the GET_TODOS query isn't notified of the new Todo object, which in turn means that the query doesn't update to show the new item.

To address this, we use cache.modify to surgically insert or delete items from the cache, by running "modifier" functions. In the example above, we know the results of the GET_TODOS query are stored in the ROOT_QUERY.todos array in the cache, so we use a todos modifier function to update the cached array to include a reference to the newly added Todo. With the help of cache.writeFragment, we get an internal reference to the added Todo, then append that reference to the ROOT_QUERY.todos array.

Any changes you make to cached data inside of an update function are automatically broadcast to queries that are listening for changes to that data. Consequently, your application's UI will update to reflect these updated cached values.

Refetching after update

An update function attempts to replicate a mutation's back-end modifications in your client's local cache. These cache modifications are broadcast to all affected active queries, which updates your UI automatically. If the update function does this correctly, your users see the latest data immediately, without needing to await another network round trip.

However, an update function might get this replication wrong by setting a cached value incorrectly. You can "double check" your update function's modifications by refetching affected active queries. To do so, you first provide an onQueryUpdated callback function to your mutate function:

addTodo({
  variables: { type: input.value },
  update(cache, result) {
    // Update the cache as an approximation of server-side mutation effects
  },
  onQueryUpdated(observableQuery) {    // Define any custom logic for determining whether to refetch    if (shouldRefetchQuery(observableQuery)) {      return observableQuery.refetch();    }  },})

After your update function completes, Apollo Client calls onQueryUpdated once for each active query with cached fields that were updated. Within onQueryUpdated, you can use any custom logic to determine whether you want to refetch the associated query.

To refetch a query from onQueryUpdated, call return observableQuery.refetch(), as shown above. Otherwise, no return value is required. If a refetched query's response differs from your update function's modifications, your cache and UI are both automatically updated again. Otherwise, your users see no change.

Occasionally, it might be difficult to make your update function update all relevant queries. Not every mutation returns enough information for the update function to do its job effectively. To make absolutely sure a certain query is included, you can combine onQueryUpdated with refetchQueries: [...]:

addTodo({
  variables: { type: input.value },
  update(cache, result) {
    // Update the cache as an approximation of server-side mutation effects.
  },
  // Force ReallyImportantQuery to be passed to onQueryUpdated.
  refetchQueries: ["ReallyImportantQuery"],
  onQueryUpdated(observableQuery) {
    // If ReallyImportantQuery is active, it will be passed to onQueryUpdated.
    // If no query with that name is active, a warning will be logged.
  },
})

If ReallyImportantQuery was already going to be passed to onQueryUpdated thanks to your update function, then it will only be passed once. Using refetchQueries: ["ReallyImportantQuery"] just guarantees the query will be included.

If you find you've included more queries than you expected, you can skip or ignore a query by returning false from onQueryUpdated, after examining the ObservableQuery to determine that it doesn't need refetching. Returning a Promise from onQueryUpdated causes the final Promise<FetchResult<TData>> for the mutation to await any promises returned from onQueryUpdated, eliminating the need for the legacy awaitRefetchQueries: true option.

To use the onQueryUpdated API without performing a mutation, try the client.refetchQueries method. In the standalone client.refetchQueries API, the refetchQueries: [...] mutation option is called include: [...], and the update function is called updateCache for clarity. Otherwise, the same internal system powers both client.refetchQueries and refetching queries after a mutation.

useMutation API

Supported options and result fields for the useMutation hook are listed below.

Most calls to useMutation can omit the majority of these options, but it's useful to know they exist. To learn about the useMutation hook API in more detail with usage examples, see the API reference.

Options

The useMutation hook accepts the following options:

Name /
Type
Description

Operation options

mutation

DocumentNode

A GraphQL query string parsed into an AST with the gql template literal.

Optional for the useMutation hook, because the mutation can also be provided as the first parameter to the hook.

Required for the Mutation component.

variables

{ [key: string]: any }

An object containing all of the GraphQL variables your mutation requires to execute.

Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value.

errorPolicy

ErrorPolicy

Specifies how the mutation handles a response that returns both GraphQL errors and partial results.

For details, see GraphQL error policies.

The default value is none, meaning that the mutation result includes error details but not partial results.

onCompleted

(data: TData | {}) => void

A callback function that's called when your mutation successfully completes with zero errors (or if errorPolicy is ignore and partial data is returned).

This function is passed the mutation's result data.

onError

(error: ApolloError) => void

A callback function that's called when the mutation encounters one or more errors (unless errorPolicy is ignore).

This function is passed an ApolloError object that contains either a networkError object or a graphQLErrors array, depending on the error(s) that occurred.

onQueryUpdated

(observableQuery: ObservableQuery, diff: Cache.DiffResult, lastDiff: Cache.DiffResult | undefined) => boolean | TResult

Optional callback for intercepting queries whose cache data has been updated by the mutation, as well as any queries specified in the refetchQueries: [...] list passed to client.mutate.

Returning a Promise from onQueryUpdated will cause the final mutation Promise to await the returned Promise. Returning false causes the query to be ignored.

refetchQueries

Array<string | { query: DocumentNode, variables?: TVariables}> | ((mutationResult: FetchResult) => Array<string | { query: DocumentNode, variables?: TVariables}>)

An array (or a function that returns an array) that specifies which queries you want to refetch after the mutation occurs.

Each array value can be either:

  • An object containing the query to execute, along with any variables
  • A string indicating the operation name of the query to refetch
awaitRefetchQueries

boolean

If true, makes sure all queries included in refetchQueries are completed before the mutation is considered complete.

The default value is false (queries are refetched asynchronously).

ignoreResults

boolean

If true, the mutation's data property is not updated with the mutation's result.

The default value is false.

Networking options

notifyOnNetworkStatusChange

boolean

If true, the in-progress mutation's associated component re-renders whenever the network status changes or a network error occurs.

The default value is false.

client

ApolloClient

The instance of ApolloClient to use to execute the mutation.

By default, the instance that's passed down via context is used, but you can provide a different instance here.

context

Record<string, any>

If you're using Apollo Link, this object is the initial value of the context object that's passed along your link chain.

Caching options

update

(cache: ApolloCache, mutationResult: FetchResult) => void

A function used to update the Apollo Client cache after the mutation completes.

For more information, see Updating the cache after a mutation.

optimisticResponse

Object

If provided, Apollo Client caches this temporary (and potentially incorrect) response until the mutation completes, enabling more responsive UI updates.

For more information, see Optimistic mutation results.

fetchPolicy

MutationFetchPolicy

Provide no-cache if the mutation's result should not be written to the Apollo Client cache.

The default value is network-only (which means the result is written to the cache).

Unlike queries, mutations do not support fetch policies besides network-only and no-cache.

Result

The useMutation result is a tuple with a mutate function in the first position and an object representing the mutation result in the second position.

You call the mutate function to trigger the mutation from your UI.

Mutate function:

Name /
Type
Description
mutate

(options?: MutationOptions) => Promise<FetchResult>

A function to trigger the mutation from your UI. You can optionally pass this function any of the following options:

  • awaitRefetchQueries
  • context
  • fetchPolicy
  • optimisticResponse
  • refetchQueries
  • update
  • variables

Any option you pass here overrides any existing value for that option that you passed to useMutation.

The mutate function returns a promise that fulfills with your mutation result.

Mutation result:

Name /
Type
Description
data

TData

The data returned from your mutation. Can be undefined if ignoreResults is true.

loading

boolean

If true, the mutation is currently in flight.

error

ApolloError

If the mutation produces one or more errors, this object contains either an array of graphQLErrors or a single networkError. Otherwise, this value is undefined.

For more information, see Handling operation errors.

called

boolean

If true, the mutation's mutate function has been called.

client

ApolloClient

The instance of Apollo Client that executed the mutation.

Can be useful for manually executing followup operations or writing data to the cache.

reset

() => void

A function that you can call to reset the mutation's result to its initial, uncalled state.

Next steps

The useQuery and useMutation hooks together represent Apollo Client's core API for performing GraphQL operations. Now that you're familiar with both, you can begin to take advantage of Apollo Client's full feature set, including:

  • Optimistic UI: Learn how to improve perceived performance by returning an optimistic response before your mutation result comes back from the server.
  • Local state: Use Apollo Client to manage the entirety of your application's local state by executing client-side mutations.
  • Caching in Apollo: Dive deep into the Apollo Client cache and how it's normalized. Understanding the cache is helpful when writing update functions for your mutations!
Edit on GitHub