Apollo Cache in a Nutshell

A Series Blog for Deep Dive into Apollo GraphQL from Backend to Frontend

Photo by Toa Heftiba on Unsplash

Apollo Cache as an Abstraction Layer

Data normalisation and Custom Identifier

const cache = new InMemoryCache({
typePolicies: {
AllProducts: {
keyFields: [],
},
Product: {
keyFields: ["upc"],
},
Person:
keyFields: ["name", "email"],
},
Book: {
keyFields: ["title", "author", ["name"]],
},
},
});
Book:{"title":"Fahrenheit 451","author":{"name":"Ray Bradbury"}}

Operations where cache can or cannot automatically update

Reading and writing data to the cache

const query = gql`
query MyTodoAppQuery {
todos {
id
text
completed
}
}
`;
const data = client.readQuery({ query });const myNewTodo = {
id: '6',
text: 'Start using Apollo Client.',
completed: false,
__typename: 'Todo',
};
client.writeQuery({
query,
data: {
todos: [...data.todos, myNewTodo],
},
});
const todo = client.readFragment({
id: 'Todo:5',
fragment: gql`
fragment MyTodo on Todo {
id
text
completed
}
`,
});
Unlike readQuery, readFragment requires an id option. This option specifies the unique identifier for the object in your cache. If you don't know about the identifier, using the utility function cache.identify()
client.writeFragment({
id: 'Todo:5',
fragment: gql`
fragment MyTodo on Todo {
completed
}
`,
data: {
completed: true,
},
});
const newComment = {
__typename: 'Comment',
id: 'abc123',
text: 'Great blog post!',
};
cache.modify({
fields: {
comments(existingCommentRefs = [], { readField }) {
const newCommentRef = cache.writeFragment({
data: newComment,
fragment: gql`
fragment NewComment on Comment {
id
text
}
`
});
// Quick safety check - if the new comment is already
// present in the cache, we don't need to add it again.
if (existingCommentRefs.some(
ref => readField('id', ref) === newComment.id
)) {
return existingCommentRefs;
}
return [...existingCommentRefs, newCommentRef];
}
}
});

Garbage collection and cache eviction

Config the TypePolicy & FieldPolicy

type TypePolicy = {
keyFields?: KeySpecifier | KeyFieldsFunction | false;
queryType?: true,
mutationType?: true,
subscriptionType?: true,
fields?: {
[fieldName: string]:
| FieldPolicy<StoreValue>
| FieldReadFunction<StoreValue>;
}
};
type KeySpecifier = (string | KeySpecifier)[];type KeyFieldsFunction = (
object: Readonly<StoreObject>,
context: {
typename: string;
selectionSet?: SelectionSetNode;
fragmentMap?: FragmentMap;
},
) => string | null | void;
const cache = new InMemoryCache({
typePolicies: {
Person: {
fields: {
userId() {
return localStorage.getItem("loggedInUserId");
},
},
},
},
});
const cache = new InMemoryCache({
typePolicies: {
Agenda: {
fields: {
tasks: {
merge(existing = [], incoming: any[]) {
return [...existing, ...incoming];
},
},
},
},
},
});
Note that existing is undefined the very first time this function is called for a given instance of the field, as the cache does not yet contain any data for the field so we are providing existing = [] default parameter.Also note that you can't push the incoming array directly onto the existing array. It must instead return a new array.
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
monthForNumber: {
keyArgs: ["number"],
},
},
},
},
});
type FieldPolicy<
TExisting,
TIncoming = TExisting,
TReadResult = TExisting,
> = {
keyArgs?: KeySpecifier | KeyArgsFunction | false;
read?: FieldReadFunction<TExisting, TReadResult>;
merge?: FieldMergeFunction<TExisting, TIncoming> | boolean;
};
type KeySpecifier = (string | KeySpecifier)[];type KeyArgsFunction = (
args: Record<string, any> | null,
context: {
typename: string;
fieldName: string;
field: FieldNode | null;
variables?: Record<string, any>;
},
) => string | KeySpecifier | null | void;
type FieldReadFunction<TExisting, TReadResult = TExisting> = (
existing: Readonly<TExisting> | undefined,
options: FieldFunctionOptions,
) => TReadResult;
type FieldMergeFunction<TExisting, TIncoming = TExisting> = (
existing: Readonly<TExisting> | undefined,
incoming: Readonly<TIncoming>,
options: FieldFunctionOptions,
) => TExisting;
interface FieldFunctionOptions {
cache: InMemoryCache;
args: Record<string, any> | null;
fieldName: string;
field: FieldNode | null;
variables?: Record<string, any>;
isReference(obj: any): obj is Reference;
toReference(
objOrIdOrRef: StoreObject | string | Reference,
mergeIntoStore?: boolean,
): Reference | undefined;
readField<T = StoreValue>(
nameOrField: string | FieldNode,
foreignObjOrRef?: StoreObject | Reference,
): T;
canRead(value: StoreValue): boolean;
storage: Record<string, any>;
mergeObjects<T extends StoreObject | Reference>(
existing: T,
incoming: T,
): T | undefined;
}

How Many Ways to Update Cache?

Option 2: const [addTodo] = useMutation(ADD_TODO, {
update(cache, { data: { addTodo } }) {
cache.modify({
fields: {
todos(existingTodos = []) {
const newTodoRef = cache.writeFragment({
data: addTodo,
fragment: gql`
fragment AddTodo on Todo {
id
type
}
`
});
return [...existingTodos, newTodoRef];
}
}
});
}
});
Option 3:
const cache = new InMemoryCache({
typePolicies: {
Mutation: {
fields: {
addTodo: {
merge(_, incoming, { cache }) {
cache.modify({
fields: {
todos(existing = []) {
return [...existing, incoming]
},
},
})
return incoming
},
},
},
},
},
})