As many of us have experienced with the Apollo Client cache, something as innocuous as adding an item to a list can be an exercise in consternation.
If you’ve used a Cell to fetch a list of data, like a user’s todos for example, then you’ve probably tried to update that data as well. The way you update that data usually looks something like this:
const [createTodo] = useMutation(CREATE_TODO, {
update(cache, { data: { createTodo } }) {
cache.modify({
fields: {
todos(existingTodoRefs = []) {
const createTodoRef = cache.writeFragment({
data: createTodo,
fragment: gql`
fragment CreateTodo on Todo {
id
type
}
`
});
return [...existingTodoRefs, createTodoRef];
}
}
});
}
});
I basically took the example above straight from the docs. And that’s just one of the ways it could’ve be done (we could’ve used cache.readQuery
(or cache.readFragment
) and cache.writeQuery
(or cache.writeFragment
)).
But why do we have to do all this anyway? Why isn’t it just as simple as any other update, where as long as CREATE_TODO
returns the id and the modified fields, it’s cached?
I’m going to try to explain 1) why it’s not that way and 2) how we can make it be that way anyway.
Why isn’t it just cached?
The short answer: it is cached, it’s just not cached as part of the todos query because that’s not how normalization works. (Maybe?)
Let’s say we have a Cell that fetches todos and displays them:
// src/components/TodosCell/TodosCell.js
export const QUERY = gql`
query TodosQuery {
todos {
id
text
}
}
`
...
export const Success = ({ todos }) => {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>• {todo.text}</li>
))}
</ul>
)
}
Nothing too fancy; at localhost:8910 we just see:
So far so good. Now let’s look at the Apollo Client cache. You could open the Apollo Client dev tools to do this, but I just came across this sweet shortcut–open the console and type:
__APOLLO_CLIENT__.cache.data.data
You should see:
And there’s the cache! Or at least what’s in it, normalized and all. Right now it’s made up of four objects: ROOT_QUERY
(our Cell’s todos
query ends up here) and three normalized todos (the normalization being all the "Todo:1"
you see going on). Notice all three of the normalized todos are part of ROOT_QUERY
's todos
. They’re all accounted for.
Now let’s add a mutation. We’ll use the useMutation
hook to add a todo and see what happens to the cache. We’ll just hard code things to keep it simple:
const CREATE_TODO = gql`
mutation CreateTodoMutation {
createTodo(text: "that thing you asked for") {
id
text
}
}
`
export const Success = ({ todos }) => {
const [createTodo] = useMutation(CREATE_TODO)
return (
<>
<button onClick={() => createTodo()}>add</button>
<ul>
{todos.map((todo) => (
<li key={todo.id}>• {todo.text}</li>
))}
</ul>
</>
)
}
Alright, now let’s press the button and inspect the cache. The Apollo docs say that the cache isn’t automatically updated when a new item is added to a list, so we’re expecting to see nothing right?
But our new todo is there! Todo:4
. So what gives?
Well that Todo:4
isn’t part of the ROOT_QUERY
's todos
. todos
is still just made up of the original three. So it seems like while the new todo was automatically cached, it wasn’t automatically added to the ROOT_QUERY
's todos
.
As to why not, again I don’t really know; I think it’s because the todos query technically was just those three Todos, and if we’re being super principled about normalized caching, it’d be wrong to assume that just because a new todo was added it’s supposed to be a part of the todos
query… ¯_(ツ)_/¯
But we obviously want todos
to be updated. As the example we kicked off with showed, we could use an update function, but there’s another way using Apollo Client 3’s TypePolicies.
TypePolicies
One of the major new features in Apollo Client 3 is TypePolicies. They’re kind of like another layer of GraphQL resolvers specifically for your client. We can use them to tell our Apollo Client cache to automatically write created todos to the todos
query:
// web/src/cache.js
import { InMemoryCache } from '@apollo/client'
const cache = new InMemoryCache({
typePolicies: {
Mutation: {
fields: {
createTodo: {
merge(_, incoming, { cache }) {
cache.modify({
fields: {
todos(existing = []) {
return [...existing, incoming]
},
},
})
return incoming
},
},
},
},
},
})
export default cache
What we’re basically saying is that whenever createTodo
is about to write data to the cache (the merge function), instead of just returning the incoming data, we want to update the todos
query with that incoming data as well. Now we can call create mutations like update mutations!
Lastly, in web/src/index.js
, pass this cache config to the RedwoodProvider
's graphQLClientConfig
prop:
...
import cache from './cache'
ReactDOM.render(
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider graphQLClientConfig={{ cache }}>
<Routes />
</RedwoodProvider>
</FatalErrorBoundary>,
document.getElementById('redwood-app')
)
Hope this made sense. If you take away one thing from this post, remember that __APOLLO_CLIENT__.cache.data.data
in the console is just