Optimistic UI
Optimistic UI is a pattern that you can use to simulate the results of a mutation and update the UI even before receiving a response from the server. Once the response is received from the server, the optimistic result is thrown away and replaced with the actual result.
Optimistic UI provides an easy way to make your UI respond much faster, while ensuring that the data becomes consistent with the actual response when it arrives.
Basic optimistic UI
Let's say we have an "edit comment" mutation, and we want the UI to update immediately when the user submits the mutation, instead of waiting for the server response. This is what the optimisticResponse
parameter to the mutate
function provides.
The main way to get GraphQL data into your UI components with Apollo is to use a query, so if we want our optimistic response to update the UI, we have to make sure to return an optimistic response that will update the correct query result. Learn more about how to do this with the dataIdFromObject
option.
Here's what this looks like in the code:
1const UPDATE_COMMENT = gql`
2 mutation UpdateComment($commentId: ID!, $commentContent: String!) {
3 updateComment(commentId: $commentId, content: $commentContent) {
4 id
5 __typename
6 content
7 }
8 }
9`;
10
11function CommentPageWithData() {
12 const [mutate] = useMutation(UPDATE_COMMENT);
13 return (
14 <Comment
15 updateComment={({ commentId, commentContent }) =>
16 mutate({
17 variables: { commentId, commentContent },
18 optimisticResponse: {
19 __typename: "Mutation",
20 updateComment: {
21 id: commentId,
22 __typename: "Comment",
23 content: commentContent
24 }
25 }
26 })
27 }
28 />
29 );
30}
1const UPDATE_COMMENT = gql`
2 mutation UpdateComment($commentId: ID!, $commentContent: String!) {
3 updateComment(commentId: $commentId, content: $commentContent) {
4 id
5 __typename
6 content
7 }
8 }
9`;
10
11const CommentPageWithData = () => (
12 <Mutation mutation={UPDATE_COMMENT}>
13 {mutate => {
14 <Comment
15 updateComment={({ commentId, commentContent }) =>
16 mutate({
17 variables: { commentId, commentContent },
18 optimisticResponse: {
19 __typename: "Mutation",
20 updateComment: {
21 id: commentId,
22 __typename: "Comment",
23 content: commentContent
24 }
25 }
26 })
27 }
28 />;
29 }}
30 </Mutation>
31);
We select id
and __typename
because that's what our dataIdFromObject
uses to determine a globally unique object ID. We need to make sure to provide the right values for those fields, so that Apollo knows what object we are referring to.
Adding to a list
In the example above, we showed how to seamlessly edit an existing object in the store with an optimistic mutation result. However, many mutations don't just update an existing object in the store, but they insert a new one.
In that case we need to specify how to integrate the new data into existing queries, and thus our UI. You can read in detail about how to do that in the article about interacting with cached data--in particular, we can use the update
function to insert a result into an existing query's result set. update
works exactly the same way for optimistic results and the real results returned from the server.
Here is a concrete example from GitHunt, which inserts a comment into an existing list of comments.
1const SUBMIT_COMMENT_MUTATION = gql`
2 mutation SubmitComment($repoFullName: String!, $commentContent: String!) {
3 submitComment(
4 repoFullName: $repoFullName
5 commentContent: $commentContent
6 ) {
7 postedBy {
8 login
9 html_url
10 }
11 createdAt
12 content
13 }
14 }
15`;
16
17function CommentsPageWithMutations({ currentUser }) {
18 const [mutate] = useMutation(SUBMIT_COMMENT_MUTATION);
19 return (
20 <CommentsPage
21 submit={(repoFullName, commentContent) =>
22 mutate({
23 variables: { repoFullName, commentContent },
24 optimisticResponse: {
25 __typename: "Mutation",
26 submitComment: {
27 __typename: "Comment",
28 postedBy: currentUser,
29 createdAt: new Date(),
30 content: commentContent
31 }
32 },
33 update: (proxy, { data: { submitComment } }) => {
34 // Read the data from our cache for this query.
35 const data = proxy.readQuery({ query: CommentAppQuery });
36 // Write our data back to the cache with the new comment in it
37 proxy.writeQuery({ query: CommentAppQuery, data: {
38 ...data,
39 comments: [...data.comments, submitComment]
40 }});
41 }
42 })
43 }
44 />
45 );
46}
1const SUBMIT_COMMENT_MUTATION = gql`
2 mutation SubmitComment($repoFullName: String!, $commentContent: String!) {
3 submitComment(
4 repoFullName: $repoFullName
5 commentContent: $commentContent
6 ) {
7 postedBy {
8 login
9 html_url
10 }
11 createdAt
12 content
13 }
14 }
15`;
16
17const CommentsPageWithMutations = ({ currentUser }) => (
18 <Mutation mutation={SUBMIT_COMMENT_MUTATION}>
19 {mutate => (
20 <CommentsPage
21 submit={(repoFullName, commentContent) =>
22 mutate({
23 variables: { repoFullName, commentContent },
24 optimisticResponse: {
25 __typename: "Mutation",
26 submitComment: {
27 __typename: "Comment",
28 postedBy: currentUser,
29 createdAt: new Date(),
30 content: commentContent
31 }
32 },
33 update: (proxy, { data: { submitComment } }) => {
34 // Read the data from our cache for this query.
35 const data = proxy.readQuery({ query: CommentAppQuery });
36 // Write our data back to the cache with the new comment in it
37 proxy.writeQuery({ query: CommentAppQuery, data: {
38 ...data,
39 comments: [...data.comments, submitComment]
40 }});
41 }
42 })
43 }
44 />
45 )}
46 </Mutation>
47);