Experimenting With Apollo 3’s New Reactive Variables for Managing Client State
Frame.io has been looking at solutions to performantly manage our increasingly complex state, and are working to adopt GraphQL into our codebase in the next version our platform. We’ve chosen to leverage Apollo Client on the front-end for managing both remote and client state. We’re also leveraging TypeScript and GraphQL-Code-Generator to help us meet our goals of reduced boilerplate, fewer bugs, and full-stack type safety.
I’m assuming by now most folks are familiar with GraphQL and its benefits, but if you aren’t, the GraphQL team’s intro is a great place to start learning. This post will focus on some cutting-edge new features of Apollo.
Understanding the Apollo Cache
We’re adopting Apollo at an interesting time. Apollo Client is on the verge of a major new release, Version 3, which provides a number of new features that developers have wanted for years. Most of these features allow for a much finer-grained control of the Apollo InMemory Cache
, the workhorse of the Apollo client, which stores the results of queries and mutations in a normalized lookup table.
Here’s a typical snapshot of what Apollo’s normalized cache looks like from the Apollo Dev Tools:
Every instance of our GraphQL VideoAsset
type returned from a query is stored here in a fast key-value lookup table. When we run new queries or mutations, the cache updates, and our client state has to keep up.
Apollo Client State in the Past
In Apollo 2.6 and earlier, managing client state involved reading and writing directly to the cache using methods like writeQuery
and readFragment
within client-side resolvers. These methods are very powerful, but also somewhat cumbersome. Simple lookups of state in the cache involve writing inline GraphQL fragments and queries, which can add up to a lot of boilerplate really fast. Since one of our goals is reduced boilerplate, this isn't ideal.
Here’s what updating a simple selected @client
field on a VideoAsset
type looks like using cache methods in Apollo 2.6. In this example we have extended the client state for our VideoAsset
type to keep track of whether or not the asset has been selected from a list:
export const resolvers = {
Mutation: {
toggleSelectedAsset: (root, vars, { getCacheKey, client, cache }) => {const id = getCacheKey({ __typename: `VideoAsset`, id: vars.id });const fragment = gql`
fragment selectedAsset on VideoAsset {
selected @client
}
`;const videoAsset = cache.readFragment<VideoAsset>({ fragment, id });const data: VideoAsset = {
...videoAsset,
selected: !videoAsset.selected,
};return !videoAsset.selected;
},
}
}
Notice how reading from the cache involves constructing a local GraphQL VideoAsset
fragment, selectedAsset
inline in our resolver. We then read that fragment out of the cache, create a mutated copy of it, and write that copy back to the cache. That's a lot of work for flipping a single boolean switch!
Thankfully Apollo 3 has a proposed solution for all this boilerplate with an as yet little-advertised feature of Apollo Client 3 called Reactive Variables.
Enter Reactive Variables
Reactive Variables were first introduced to Apollo 3 in this PR which provides a new method on InMemoryCache
to instantiate a local value that the cache is aware of, makeVar()
. This method returns a function which can be called with no arguments to get the variable's value, or with a single argument to set the variable's value. The power of Reactive Variables is in consuming their values while reading from the cache, and then being able to change those values from anywhere, triggering immediate updates to any components that depend on that part of the cache.
Let’s create a new Reactive Variable to keep track of our selected VideoAssets
:
const cache = new InMemoryCache();
export const selectedAssets = cache.makeVar<Record<string, boolean>>({});
We pass in an empty object and tell TypeScript that it’s a Record of string ID keys mapping to boolean values. This provides handy type-safety anywhere we would invoke the newly produced selectedAssets()
getter/setter function. Now let's put it to use by updating our mutation resolver to update selectedAssets()
instead of using readFragment
:
export const resolvers = {
Mutation: {
toggleSelected: (_root, { id }) => {
selectedAssets({ ...selectedAssets(), [id]: !selectedAssets()[id] });
return selectedAssets()[id];
},
}
}
Much simpler! Notice how we’re passing selectedAssets()
a mutated copy of its current value, with a toggled boolean at the id
key we get from our mutation resolver's variables argument. As soon as selectedAssets()
is invoked with a new value, Apollo broadcasts updates to the cache, causing any of our components that consume query hooks that depend on it to re-fire with updated data.
Consuming Reactive Variables in Type Policies
To pass the value of our Reactive Variable down through a query we use Apollo 3’s new TypePolicies, which give us field-level control over query responses. We can set a TypePolicy for our VideoAsset
type to consume from our new Reactive Variable like so:
const typePolicies = {
VideoAsset: {
fields: {
selected: (_existing, { readField }) => {
const id = readField<string>('id');
const selected = selectedAssets();
if (!(id in selected)) selected[id] = false;
return selected[id];
},
}
}
}
Apollo calls the selected
function above any time it encounters a selected
field in a query that includes a VideoAsset
. The type policy here simply reads the VideoAsset's id
and returns its corresponding boolean value from selected, instantiating it to false
if the id has not been set before. Pretty simple!
Early Days
Reactive Variables are still a more or less undocumented feature of Apollo Client 3 Beta, but they already provide a tantalizingly streamlined approach to handling client state with Apollo. It remains to be seen how much of the previous Apollo state management they’ll actually be able to replace, but early experiments by our team have proved promising.
If you’re interested in learning more, Apollo have recently published a best practices repo with a fleshed out example using Reactive Variables in a TODO list application. Check it out here. What ways have you looked at to improve state management with less code?