Mutations and Optimistic UI in Apollo Client
Slava Kim
Apollo Client is a simple yet functional GraphQL client that works with any front-end and any GraphQL spec-compliant server.
Part of the reason developers choose to work with GraphQL, a fairly new technology, is the ease of querying nested application data while specifying only the fields that you need.
But hardly any application on the modern web is read-only. Handling users’ interactions often requires modifications to server-side data, keeping the browser’s state in sync, while looking fast and responsive to any actions.
As of a recent release, Apollo Client now provides simple tools to work with mutations while keeping store updates and rerenders efficient.
Interested in learning more about Apollo and our future goals? Ask questions during our AMA on Hashnode on July 26, 2016 — it starts at 11 AM Pacific time!
Firing a Mutation
A typical GraphQL mutation has two major parts: what mutation to call with what parameters, and what the mutation should return on its success.
Here is what a mutation call looks like in Apollo Client using the GraphQL mutation syntax:
client.mutate({ mutation: gql` mutation ($text: String!, $list_id: ID!) { addNewTask(text: $text, list_id: $list_id) { id text completed createdAt } } `, variables: { text: 'walk the dog', list_id: '123', }, })
It is a standard GraphQL mutation query string, with arguments passed via variables — just like what you would type in GraphiQL.
Updating Query Results
After a mutation has fired and the data has changed on the server, users expect all parts of the UI to reflect the changes that they have made. If we don’t need to be efficient, the easiest way to do this is simply to refetch all of the queries we are currently looking at. But if we want to transfer the minimal information over the wire to keep the client in sync, we need to manually handle the mutation return value and update our results in a fine-grained way.
Apollo Client now gives you a way to update any queries currently being watched with the result of a mutation. This way, you can append a newly created object to the correct location, or update some specific fields without having to refetch anything.
Let’s look at an example. Let’s say there is a query that fetches the list of tasks for the currently selected list:
query todos($list_id: ID!) { todo_list(id: $list_id) { title tasks { id text completed createdAt } } }
… then when you call a mutation creating a new task, you can easily update the query above using the new updateQueries option to client.mutate:
client.mutate({ mutation: ..., // same as above variables: ..., // same as above updateQueries: { todos: (previousQueryResult, { mutationResult }) => { return { title: previousQueryResult.title, tasks: [...previousQueryResult.tasks, mutationResult], }; }, }, });
You can see above that updating a query result is as simple as writing a reducer function, a concept borrowed from Redux reducers: it is a function that takes the old query result, a new object (in this case, the mutation result) and combines them together into the new query result.
Optimistic UI
The story about mutations would have ended here if the Internet operated with zero latency from any part of the world. Unfortunately, that’s not the case, which means that every communication with the server takes a significant delay.
Users have grown to expect an instant response to most trivial interactions with a website or an app. They expect the UI to react immediately and the changes to persist in the background.
Turns out, this is something an application can deliver most of the time. Whenever we create a new task in a todo list, or remove an image from a grid — we can simulate the result by guessing what will happen, and then validate the behavior with the server. The trick is to update the UI state with a fake response first, and then re-validate it once the server responses. This is what we call Optimistic UI.
Apollo Client gives a very simple way to specify what is the “guesstimate” response from the server. The optimistic update is always rolled back once we find out the real result from the server, so the user sees the “fake” only until the request to the server returns or times out.
To specify the predicted server result, just use the new optimisticResponse option to client.mutate:
client.mutate({ mutation: ..., // same as above variables: ..., // same as above updateQueries: ..., // same as above optimisticResponse: { id: generatedId, text: text, // this is one of the arguments createdAt: +(new Date), // not accurate, but close completed: false, // assume task is created not completed }, });
Minimal updates under the hood
As you might have noticed, your reducer function always operates on the query result, and not some normalized format. But don’t be fooled! Internally, Apollo Client normalizes the results of both queries and mutation updates into a store, to get benefits such as query caching and consistency across the store. You can open up Redux dev-tools and inspect how Apollo Client’s integrates these results into the internal state format.
Conclusion
Apollo Client now gives application developers convenient tools to work with GraphQL mutations, taking care of maintaining the intermediate state, applying an optimistic result, and finally replacing it with a server response.
Importantly, Apollo Client provides a familiar interface (reducer functions) to a mechanism to keep the queries used on the screen in sync after every mutation, so you don’t have to learn about cache internals to update your screen.