loading...

Update Apollo Cache after a mutation and get instant benefits on your UI

lucis profile image Lucis ・5 min read

I'm very fond of the products developed by the Apollo team, specially the Apollo Client. It started as a simple tool to connect your frontend data requirements with a GraphQL server but, nowadays, it's a complete solution for data management on the client-side. Even though using GraphQL potentially reduces the need for local state, Apollo comes with a redux-like "global" store that you access it with a GraphQL interface. If you want to know more about how to implement client-state with Apollo, read here.

I don't want to talk about local resolvers or frontend-only state, but the underlying cache that Apollo uses when you fetch GraphQL from the server, specially how you can leverage it to make your UIs more responsive and smart. Apollo docs on how to interact with cache is a good resource but, IMO, a bit far from real use cases.

Simple CRUD

I'll create a simple Wishlist React app (though most of what is shown is applicable to other frameworks), that is simply a CRUD of Items { title: string, price: int }. Like most real apps it has a list of resources, a delete button, a edit form and an add form.What I want to show is, basically, how to update the list after a mutation without having to refetch the list from the server.

I'll be using this simple server that I've created with Codesandbox (man, aren't these guys awesome?) and the only thing to notice is that, on the edit mutation, your server should return the updated resource, you'll know why later. I'll add some mock data for us to see the results right-away.

I'll assume that you already know how to setup an Apollo Client + React Apollo application. If not, check it here.

So, I'll go over every letter on CRUD (for educational purposes, RUCD) and explain what you may need to do to use.

Read

const GET_ITEMS = gql`
  {
    items {
      id
      title
      price
    }
  }
`;

function Wishlist() {
  const { loading, error, data } = useQuery(GET_ITEMS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return data.items.map(item => {
    const { id, title, price } = item;
    return (
      <div key={id}>
        {title} - for ${price} -{" "}
      </div>
    );
  });
}

This is it! Our read query is very simple: it doesn't take search parameters, pagination metadata or uses cursors. For demonstration purposes it's better this way, but I'll comment later if you need some help with more complex cases. Just with this setup, we're already prepared for our list to react to cache changes on subsequent writes.

img

Note: Using useQuery, <Query> or even withQuery is fundamental here, since it links the data with the cache. If you use other solution like useApolloClient and, then, client.query, these solutions will not work for you.

Edit

We will create our Edit component and this one is the easiest case because you don't need to do anything for the list to auto update after you edit the Item. Apollo Cache, by default, uses the pair __typename + id (or _id) as a primary key to every cache object. Every time the server returns data, Apollo checks if this new data replaces something that it has on its cache, so, if your editItem mutation returns the complete updated Item object, Apollo will see that it already has that item on the cache (from when you fetched for the list), update it, and triggers the useQuery to update the data, subsequently making our Wishlist re-render.

const EDIT_ITEM = gql`
  mutation($id: Int, $item: ItemInput) {
    editItem(id: $id, item: $item) {
      id
      title
      price
    }
  }
`;

const EditItem = ({ item: { title, price, id } }) => {
  const [editItem, { loading }] = useMutation(EDIT_ITEM);

  return (
    <ItemForm
      disabled={loading}
      initialPrice={price}
      initialTitle={title}
      onSubmit={item => {
        editItem({
          variables: {
            id,
            item
          }
        });
      }}
    />
  );
};

Done! When the user hit the Submit button on the edit form, the mutation will trigger, the Apollo client will receive the updated data and that item will update automatically on the list.

Create

Let's create our AddItem component. This time it's a bit different because when we get our response from the server (the newly created Item) Apollo doesn't "know" that the list should be updated with the new item (and, sometime, it shouldn't). For this we have to programatically add our new item to the list, and one of the parameters of the useMutation hook is the update function, that is there specifically for that purpose.

The steps we need to update the cache are:

  1. Read the data from Apollo cache (we will use the same GET_ITEMS query)
  2. Update the list of items pushing our new item
  3. Write the data back to Apollo cache (also referring to the GET_ITEMS query)

After this, Apollo Client will notice that the cache for that query has changed and will also update our Wishlist on the end.

const ADD_ITEM = gql`
  mutation($item: ItemInput) {
    addItem(item: $item) {
      id
      title
      price
    }
  }
`;

const AddItem = () => {
  const [addItem, { loading }] = useMutation(ADD_ITEM);

  return (
    <ItemForm
      disabled={loading}
      onSubmit={item => {
        addItem({
          variables: {
            item
          },
          update: (cache, { data: { addItem } }) => {
            const data = cache.readQuery({ query: GET_ITEMS });
            data.items = [...data.items, addItem];
            cache.writeQuery({ query: GET_ITEMS }, data);
          }
        });
      }}
    />
  );
};

Delete

The delete case is very similar to the create-one. I will colocate it on the Wishlist component for simplicity, and also add some props/state for the overall functionality of the app.

const DELETE_ITEM = gql`
  mutation($id: Int) {
    deleteItem(id: $id) {
      id
      title
      price
    }
  }
`;

function Wishlist({ onEdit }) {
  const { loading, error, data } = useQuery(GET_ITEMS);
  const [deleteItem] = useMutation(DELETE_ITEM);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return data.items.map(item => {
    const { id, title, price } = item;
    return (
      <div key={id}>
        {title} - for ${price} -{" "}
        <button
          className="dim pointer mr2"
          onClick={e => {
            onEdit(item);
          }}
        >
          edit
        </button>
        <button
          className="dim pointer"
          onClick={() => {
              deleteItem({ variables: { id }, 
              update: cache => {
                const data = cache.readQuery({ query: GET_ITEMS });
                data.items = data.items.filter(({id: itemId}) => itemId !== id);
                cache.writeQuery({ query: GET_ITEMS }, data);
              }});
          }}
        >
          delete
        </button>
      </div>
    );
  });
}

Advanced cases

It's common to have more complex lists on our apps with search, pagination and ordering and, for those, it gets a bit complicated and it heavily depends on the context of your application. For example, when deleting an item of the 4th page of items, should we delete it from the UI and show pageLength - 1 items or fetch one item from the next page and add it?

These cases are also tricky because the cache.readQuery also need variables if the query provided receives it, and you might not have it globally available. One option is using something like refetch from the original query, or refetchQueries from Apollo, what makes it go on the server again. If you want to dig deeper on this problem, this issue has a lot of options for you, specially a snippet for getting a query's last used variables.

The final solution

The app has some state/UI quirks, I've made the minimal to demonstrate Apollo Cache Update :)

The whole app developed here is on this CodeSandbox. You might need to fork this container and update the server URL.

img

Feel free to reach me :)

Discussion

pic
Editor guide
Collapse
jeremyhunter profile image
Jeremy Hunter

This is what the Apollo docs for local state should look like - much easier to follow for real use cases. It seems like it will get a bit convoluted, but not unmanageable, when paired with optimisticResponse since they both interact with the cache. Thanks :)

Collapse
cautioncreation profile image
Byron Hill

Great post!

As per the Apollo-Client docs "Do not modify the return value of readQuery. The same object might be returned to multiple components. To update data in the cache, instead create a replacement object and pass it to writeQuery."

I attempted to use your solution for adding to a list of a users chats on creation and ran into "Error: Cannot assign to read only property 'edges' of object '#'"

Once again, great post, just thought I'd put this here for anyone running into similar issues. You must create a new object, you cannot mutate the return value of cache.readQuery anymore and apollo will throw an error.

Collapse
ephremdeme profile image
Ephrem Demelash

Helpful post and thanks for sharing. here is my package npmjs.com/package/mutation-cache-u... motivated by you.

Collapse
lucis profile image
Lucis Author

Very nice, Ephrem!

Collapse
kimobrian254 profile image
Warrior254

I've run into some issues and no one seems to respond either in the apollo community, graphql or redit. The issue is cache update but for a paginated api result. Here's the problem as I wrote it on reddit(reddit.com/r/graphql/comments/frr7...)

I'm running into an issue with cache updates. I've a list of posts fetched using a query FETCH_POSTS which returns an array of type Post. I also have a CREATE_POST and UPDATE_POST mutations which return an item of type Post too. When I create a post, and use writeQuery to update the cache it works ok. The new object will be [...existingPosts, newPost]. When it comes to updating a post. When I readQuery and log the result, it seems to have updated the cache already(I'm guessing it's an auto update since the type and id are the same and exist) so writeQuery in that situation doesn't make sense. The only issue is these updates do not appear on the UI. Any idea why I may be experiencing this? This implementation works when the list of posts is not paginated. But on pagination the edit to the item in the list does not work. This is my query to fetch posts using the connection directive to ignore the pagination params.

const FETCH_POSTS = gql query getPosts($params: FetchPostsParams!, $pagination: PaginationParams) { posts(params: $params, pagination: $pagination) @connection(key: "posts", filter: ["params"]) { ...PostFragment } } ${FRAGMENTS.POST_FRAGMENT} ;

Collapse
lucis profile image
Lucis Author

Things like this usually happen because the query is cached paired with the pagination variables... There was a solution I've found on Github Issues years ago where you could retrieve the "last used params" for the pagination, and, with that, your cache update would work just fine.

But, I see it's a simple update, the resolutions algorithm should work just fine, it's the easiest of cases. One thing you can look into is the use of Fragments, I've never used it, so it might be the point of the problem.

Collapse
maryvorontsov profile image
Mary Vorontsov

Hey Luciano! Will you be interested in a paid writing opportunity in your spare time?

Collapse
lucis profile image
Lucis Author

Hey Mary!

How would that work? It depends on the load, I guess...

Collapse
maryvorontsov profile image
Mary Vorontsov

sure. do you have skype or twitter? my twitter @maryvorontsov