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:
1import { gql, useMutation } from '@apollo/client';
2
3// Define mutation
4const INCREMENT_COUNTER = gql`
5 # Increments a back-end counter and gets its resulting value
6 mutation IncrementCounter {
7 currentValue
8 }
9`;
10
11function MyComponent() {
12 // Pass mutation to useMutation
13 const [mutateFunction, { data, loading, error }] = useMutation(INCREMENT_COUNTER);
14}
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:
1import { gql, useMutation } from '@apollo/client';
2
3const ADD_TODO = gql`
4 mutation AddTodo($type: String!) {
5 addTodo(type: $type) {
6 id
7 type
8 }
9 }
10`;
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:
1function AddTodo() {
2 let input;
3 const [addTodo, { data, loading, error }] = useMutation(ADD_TODO);
4
5 if (loading) return 'Submitting...';
6 if (error) return `Submission error! ${error.message}`;
7
8 return (
9 <div>
10 <form
11 onSubmit={e => {
12 e.preventDefault();
13 addTodo({ variables: { type: input.value } });
14 input.value = '';
15 }}
16 >
17 <input
18 ref={node => {
19 input = node;
20 }}
21 />
22 <button type="submit">Add Todo</button>
23 </form>
24 </div>
25 );
26}
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
:
1const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
2 variables: {
3 type: "placeholder",
4 someOtherVariable: 1234,
5 },
6});
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:
1addTodo({
2 variables: {
3 type: input.value,
4 },
5});
Here, we use the variables
option to provide the values of any GraphQL variables that our mutation 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 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 type
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:
1if (loading) return 'Submitting...';
2if (error) return `Submission error! ${error.message}`;
The
useMutation
hook also supportsonCompleted
andonError
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:
1const [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 theuseMutation
hook, causing the corresponding component to rerender.
1function LoginPage () {
2 const [login, { error, reset }] = useMutation(LOGIN_MUTATION);
3
4 return (
5 <>
6 <form>
7 <input class="login"/>
8 <input class="password"/>
9 <button onclick={login}>Login</button>
10 </form>
11 {
12 error &&
13 <LoginFailedMessageWindow
14 message={error.message}
15 onDismiss={() => reset()}
16 />
17 }
18 </>
19 );
20}
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:
1// Refetches two queries after mutation completes
2const [addTodo, { data, loading, error }] = useMutation(ADD_TODO, {
3 refetchQueries: [
4 GET_POST, // DocumentNode object parsed with gql
5 'GetComments' // Query name
6 ],
7});
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 thegql
functionThe 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 have 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:
1{
2 "__typename": "Todo",
3 "id": "5",
4 "type": "groceries"
5}
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:
1const GET_TODOS = gql`
2 query GetTodos {
3 todos {
4 id
5 }
6 }
7`;
8
9function AddTodo() {
10 let input;
11 const [addTodo] = useMutation(ADD_TODO, {
12 update(cache, { data: { addTodo } }) {
13 cache.modify({
14 fields: {
15 todos(existingTodos = []) {
16 const newTodoRef = cache.writeFragment({
17 data: addTodo,
18 fragment: gql`
19 fragment NewTodo on Todo {
20 id
21 type
22 }
23 `
24 });
25 return [...existingTodos, newTodoRef];
26 }
27 }
28 });
29 }
30 });
31
32 return (
33 <div>
34 <form
35 onSubmit={e => {
36 e.preventDefault();
37 addTodo({ variables: { type: input.value } });
38 input.value = "";
39 }}
40 >
41 <input
42 ref={node => {
43 input = node;
44 }}
45 />
46 <button type="submit">Add Todo</button>
47 </form>
48 </div>
49 );
50}
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:
1addTodo({
2 variables: { type: input.value },
3 update(cache, result) {
4 // Update the cache as an approximation of server-side mutation effects
5 },
6 onQueryUpdated(observableQuery) {
7 // Define any custom logic for determining whether to refetch
8 if (shouldRefetchQuery(observableQuery)) {
9 return observableQuery.refetch();
10 }
11 },
12})
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: [...]
:
1addTodo({
2 variables: { type: input.value },
3 update(cache, result) {
4 // Update the cache as an approximation of server-side mutation effects.
5 },
6 // Force ReallyImportantQuery to be passed to onQueryUpdated.
7 refetchQueries: ["ReallyImportantQuery"],
8 onQueryUpdated(observableQuery) {
9 // If ReallyImportantQuery is active, it will be passed to onQueryUpdated.
10 // If no query with that name is active, a warning will be logged.
11 },
12})
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:
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).
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.
boolean
If true
:
The initial state update (setting loading to true) is skipped - The success state update (setting data and setting loading to false) is skipped - Error updates will still occur
The default value is false
.
This option is useful when you want to execute a mutation but don't need to track its progress or result in the UI, potentially improving performance by reducing re-renders.
(data: TData, clientOptions?: BaseMutationOptions) => 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
and any options passed to the mutation.
(error: ApolloError, clientOptions?: BaseMutationOptions) => 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, as well as any options passed the mutation.
OnQueryUpdated<any>
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.
((result: FetchResult<TData>) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude
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 anyvariables
A string indicating the operation name of the query to refetch
TVariables
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.
ApolloClient<object>
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.
TContext
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 mutation's associated component re-renders whenever the network status changes or a network error occurs.
The default value is false
.
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
.
TData | ((vars: TVariables, { IGNORE }: {
IGNORE: IgnoreModifier;
}) => TData | IgnoreModifier)
By providing either an object or a callback function that, when invoked after a mutation, allows you to return optimistic data and optionally skip updates via the IGNORE
sentinel object, 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.
MutationUpdaterFunction<TData, TVariables, TContext, TCache>
A function used to update the Apollo Client cache after the mutation completes.
For more information, see Updating the cache after a mutation.
boolean
To avoid retaining sensitive information from mutation root field arguments, Apollo Client v3.4+ automatically clears any ROOT_MUTATION
fields from the cache after each mutation finishes. If you need this information to remain in the cache, you can prevent the removal by passing keepRootFields: true
to the mutation. ROOT_MUTATION
result data are also passed to the mutation 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 query names to mutation 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 mutation result in the second position.
You call the mutate function to trigger the mutation from your UI.
boolean
If true
, the mutation's mutate function has been called.
ApolloClient<object>
The instance of Apollo Client that executed the mutation.
Can be useful for manually executing followup operations or writing data to the cache.
TData | null
The data returned from your mutation. Can be undefined
if ignoreResults
is true
.
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.
boolean
If true
, the mutation is currently in flight.
() => 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!