Optimizing list item deletion with Apollo’s @client directive and fragments

Renaud Bellec
Inside Hexa
Published in
4 min readMay 7, 2020

--

When deleting a single item from a remote list of data, the server would usually return its ID if the deletion was successful.

In order to update the list, you’d need to go through it until your find the deleted item, remove it, and then write the list purged from this unwanted item into Apollo’s cache. With large lists, this could be a costly operation and it could even throw an error if the query you are trying to write doesn’t exist.

With the @client directive, you can mix remote and local data without having to iterate over the list. Let’s see how.

Disclaimer: this article assumes that you are somewhat familiar with Apollo and you have a project up and running.

Typical implementation without optimistic logic

Let’s say we want to build a catalogue of bicycles from the mighty Genesis brand!

You’d query a list of bicycle like:

const GET_BICYClES = gql`
query getBicyles {
bicycles {
id
name
}
}
`;

And delete an item like this:

const DELETE_BICYCLE = gql`
mutation deleteBicycle($id: ID!) {
deleted: deleteBicycle(id: $id) {
id
}
}
`;
const [deleteMutation] = useMutation(DELETE_BICYCLE);deleteMutation({
variables: {
id: theBicycleYouWantToDeleteId,
},
optimisticResponse: {
__typename: 'Mutation',
deleted: {
id: theBicycleYouWantToDeleteId,
},
},
});

Now, to get the new list without the deleted item, we could refetch the entire list from the remote source:

const [deleteMutation] = useMutation(
DELETE_BICYCLE,
{
refetchQueries: [{ query: GET_BICYCLES }],
},
);

But depending on the user’s network, this could take a few seconds before actually updating the UI. This is not the smoothest experience, is it?

Another solution would be to update the list cached by Apollo.

Optimization by updating the cached list

To do so, we’d pass an update function to our mutation’s options and use the readQuery and writeQuery methods provided by the Apollo cache:

deleteMutation({
variables: {
id: theBicycleYouWantToDeleteId,
},
optimisticResponse: {
__typename: 'Mutation',
deleted: {
id: theBicycleYouWantToDeleteId,
},
},
update(cache, { data: { deleted: { id } }) {
// First, we get the list.
const queryResult = cache.readQuery({
query: GET_BICYCLES,
});
// Then update que query with a list cleared from the
// deleted item.
if (queryResult) {
cache.writeQuery({
query: GET_BICYCLES,
data: {
bicycles: queryResult.data.bicycles.filter(
bicycle => bicyle.id !== id
)
},
});
}
}
});

So this works but might encounter a few issues:

  • We need to query and write into the cache. With large lists, it can be a costly operation.
  • We might use some variables for your query such as a pagination system. Apollo will throw if your query with the exact variables doesn’t exist in the cache.
  • We are updating only one query here and your bicycle could exist in another one and still be visible somewhere else in our app.

Optimization with the @client directive

Apollo provides a @client directive in order to manage local data. You can use it to manage the state of your application instead of using a tool like Mobx or Redux (although they are both lovable pieces of software). You can even mix it up with remote data!

// First, we create a fragment on Bicycle with the client property.
const BicycleFragment = gql`
fragment ClientBicycle on Bicycle {
id
name
_deleted @client
}
`;
const GET_BICYClES = gql`
query getBicyles {
bicycles {
...ClientBicycle
}
}
${BicycleFragment}
`;

Here, we added a _deleted property to our bicycles in the GET_BICYCLES query to be able to mark one as deleted and immediately update the UI without having to write an entire list into the cache.

But Apollo doesn’t know yet what to do with this property. We must tell it how to resolve it with a custom resolver that we’ll provide to our ApolloClient configuration:

// ...const client = new ApolloClient({
cache,
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
}),
resolvers: {
Bicycle: { // You can tell ApolloClient how to resolve a property \o/ !
_deleted: bicycle => Boolean(bicycle._deleted),
}
},
});
// ...

Now that ApolloClient knows, We need to update our mutation call to use the fragment to handle our optimistic response:

deleteMutation({
variables: {
id: theBicycleYouWantToDeleteId,
},
optimisticResponse: {
__typename: 'Mutation',
deleted: {
id: theBicycleYouWantToDeleteId,
},
},
update(cache, { data: { deleted: { id } }) {
// We get a single item.
const bicycle = client.readFragment({
id: `Bicycle:${id}`,
fragment: BicycleFragment,
});
// Then, we update it.
if (bicycle) {
client.writeFragment({
id: `Bicycle:${id}`,
fragment: BicycleFragment,
data: {
...bicycle,
_deleted: true,
},
});
}
}
});

Now, our bicycle is marked as _deleted!

Now that we have a way to differentiate bicycle that should be deleted, we can update our UI:

<ul>
{bicycles.filter(b => !b._deleted).map(b => (
<li key={b.id}>{b.name}</li>
))}
</ul>

Conclusion

Today, one can’t just pop an item from an Apollo query but it doesn’t mean that we have to iterate over the entire list of data until we find the item to be deleted and write a new list into the cache. With the @client directive and the use of fragment, optimistic deletion is blazingly fast!

What’s next?

At Folk we used this to handle complex sets of contacts. This helps us offer our users a smooth experience, so they can be productive in managing their network.

Apollo is a very powerful tool and we really enjoyed the ride trying to get to its full potential. If you feel like exploring its capabilities while building a wonderful software, feel free to get in touch with us 👉 https://www.efounders.com/jobs?company=folk !

--

--