Docs
Launch GraphOS Studio

Mutations in Apollo Client

Modify data with the useMutation hook


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

This article demonstrates how to send updates to your with the useMutation hook. You'll also learn how to update the cache after executing a , 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 . If you need a refresher, we recommend that you read this guide.

This article also assumes that you've already set up 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 in an Apollo application.

To execute a , you first call useMutation within a React component and pass it the 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 string into a 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
    • Unlike useQuery, useMutation doesn't execute its automatically on render. Instead, you call this mutate function.
  • An object with that represent the current status of the '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 named ADD_TODO. Remember to wrap strings in the gql function to parse them into :

add-todo.jsx
import { gql, useMutation } from '@apollo/client';
const ADD_TODO = gql`
mutation AddTodo($type: String!) {
addTodo(type: $type) {
id
type
}
}
`;

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 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: { type: 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 to execute the by sending it to our .

Note that this behavior differs from useQuery, which executes its as soon as its component renders. This is because 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 variables:

const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
variables: {
type: "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: {
type: input.value,
},
});

Here, we use the variables option to provide the values of any that our requires (specifically, the type 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 provided only to useMutation are preserved in the resulting object. This helps you set default values for .

In the example snippets above, input.value would override "placeholder" as the value of the type . 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 's execution. The of this object (listed in Result) include booleans that indicate whether the mutate function has been called yet, and whether the 's result is currently loading.

The example above destructures the loading and error from this object to render the AddTodo component differently depending on the '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 result object returned by useMutation includes a reset function:

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

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

Calling reset does not remove any cached data returned by the '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 , 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 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 . However, this method requires additional network requests.

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

If you're just getting started with , 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 , you can include a refetchQueries array in that '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
],
});

You can only refetch active queries. Active queries are those used by components on the current page. If the data you want to update is not fetched by a component on the current page, it's best to update your cache directly.

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

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

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

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 .

Updating the cache directly

Include modified objects in mutation responses

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

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

{
"__typename": "Todo",
"id": "5",
"type": "groceries"
}

automatically adds the __typename to every object in your queries and by default.

Upon receiving this response object, caches it with key Todo:5. If a cached object already exists with this key, overwrites any existing that are also included in the 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 in your cache (such as certain list fields), you can define an update function to apply manual changes to your cached data after a .

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 cache. This object provides access to cache API methods like readQuery/writeQuery, readFragment/writeFragment, modify, and evict. These methods enable you to execute on the cache as though you're interacting with a .

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 . You can use this value to update the cache with cache.writeQuery, cache.writeFragment, or cache.modify.

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

When the ADD_TODO 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 ) is not automatically updated. This means that the GET_TODOS isn't notified of the new Todo object, which in turn means that the 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 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 '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, 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 .

To refetch a from onQueryUpdated, call return observableQuery.refetch(), as shown above. Otherwise, no return value is required. If a refetched '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 returns enough information for the update function to do its job effectively. To make absolutely sure a certain 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 will be included.

If you find you've included more queries than you expected, you can skip or ignore a 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 to await any promises returned from onQueryUpdated, eliminating the need for the legacy awaitRefetchQueries: true option.

To use the onQueryUpdated API without performing a , try the client.refetchQueries method. In the standalone client.refetchQueries API, the refetchQueries: [...] 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 .

useMutation API

Supported options and result 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:

Operation options

awaitRefetchQueries (optional)

boolean

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

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

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

For details, see GraphQL error policies.

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

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

The default value is false.

(data: TData, clientOptions?: BaseMutationOptions) => void

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

This function is passed the 's result data and any options passed to the .

(error: ApolloError, clientOptions?: BaseMutationOptions) => void

A callback function that's called when the 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, as well as any options passed the .

OnQueryUpdated<any>

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

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

((result: FetchResult<TData>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude

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

Each array value can be either:

  • An object containing the query to execute, along with any variables

  • A string indicating the of the to refetch

An object containing all of the your requires to execute.

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

Networking options
ApolloClient<object>

The instance of ApolloClient to use to execute the .

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

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

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

The default value is false.

Caching options
MutationFetchPolicy

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

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

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

TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; }) => TData)

By providing either an object or a callback function that, when invoked after a , allows you to return optimistic data and optionally skip updates via the IGNORE sentinel object, caches this temporary (and potentially incorrect) response until the completes, enabling more responsive UI updates.

For more information, see Optimistic mutation results.

MutationUpdaterFunction<TData, TVariables, TContext, TCache>

A function used to update the cache after the completes.

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

Other

To avoid retaining sensitive information from root , v3.4+ automatically clears any ROOT_MUTATION from the cache after each finishes. If you need this information to remain in the cache, you can prevent the removal by passing keepRootFields: true to the . ROOT_MUTATION result data are also passed to the update function, so we recommend obtaining the results that way, rather than using this option, if possible.

MutationQueryReducersMap<TData>

A MutationQueryReducersMap, which is map from names to query reducers. Briefly, this map defines how to incorporate the results of the mutation into the results of queries that are currently being watched by your application.

Result

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

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

Other
boolean

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

ApolloClient<object>

The instance of that executed the .

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

TData | null

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

ApolloError

If the 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.

boolean

If true, the is currently in flight.

() => void

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

Next steps

The useQuery and useMutation hooks together represent 's core API for performing . 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 result comes back from the server.
  • Local state: Use to manage the entirety of your application's local state by executing client-side .
  • Caching in Apollo: Dive deep into the cache and how it's normalized. Understanding the cache is helpful when writing update functions for your !
Previous
Suspense
Next
Refetching
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company