@apollo/client
Version:
A fully-featured caching GraphQL client.
561 lines (464 loc) • 10.9 kB
Markdown
# Caching Reference
## Table of Contents
- [InMemoryCache Setup](#inmemorycache-setup)
- [Cache Normalization](#cache-normalization)
- [Type Policies](#type-policies)
- [Field Policies](#field-policies)
- [Pagination](#pagination)
- [Cache Manipulation](#cache-manipulation)
- [Garbage Collection](#garbage-collection)
## InMemoryCache Setup
### Basic Configuration
```typescript
import { InMemoryCache } from "@apollo/client";
const cache = new InMemoryCache({
// Custom type policies
typePolicies: {
Query: {
fields: {
// Query-level field policies
},
},
User: {
keyFields: ["id"],
fields: {
// User-level field policies
},
},
},
// Custom type name handling (rare)
possibleTypes: {
Character: ["Human", "Droid"],
Node: ["User", "Post", "Comment"],
},
});
```
### Constructor Options
```typescript
new InMemoryCache({
// Define how types are identified in cache
typePolicies: {
/* ... */
},
// Interface/union type mappings between supertypes and their subtypes
possibleTypes: {
/* ... */
},
// Custom function to generate cache IDs (rare)
dataIdFromObject: (object) => {
if (object.__typename === "Book") {
return `Book:${object.isbn}`;
}
return defaultDataIdFromObject(object);
},
});
```
## Cache Normalization
Apollo Client normalizes data by splitting query results into individual objects and storing them by unique identifier.
### How Normalization Works
```graphql
# Query
query GetPost {
post(id: "1") {
id
title
author {
id
name
}
}
}
```
```typescript
// Normalized cache structure
{
'Post:1': {
__typename: 'Post',
id: '1',
title: 'Hello World',
author: { __ref: 'User:42' }
},
'User:42': {
__typename: 'User',
id: '42',
name: 'John'
},
ROOT_QUERY: {
'post({"id":"1"})': { __ref: 'Post:1' }
}
}
```
### Benefits of Normalization
1. **Automatic updates**: When `User:42` is updated anywhere, all components showing that user update
2. **Deduplication**: Same objects aren't stored multiple times
3. **Efficient updates**: Only changed objects trigger re-renders
## Type Policies
### keyFields
Customize how objects are identified in the cache.
```typescript
const cache = new InMemoryCache({
typePolicies: {
// Use ISBN instead of id for books
Book: {
keyFields: ["isbn"],
},
// Composite key
UserSession: {
keyFields: ["userId", "deviceId"],
},
// Nested key
Review: {
keyFields: ["book", ["isbn"], "reviewer", ["id"]],
},
// No key fields (singleton, only one object in cache per type)
AppSettings: {
keyFields: [],
},
// Disable normalization (objects of this type will be stored with their
// parent entity. The same object might end up multiple times in the cache
// and run out of sync. Use with caution, only if this object really relates
// to a property of their parent entity and cannot exist on its own.)
Address: {
keyFields: false,
},
},
});
```
### merge Functions
Control how incoming data merges with existing data.
```typescript
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
// Deep merge profile object
profile: {
merge: true, // Shorthand for deep merge
},
// Custom merge logic
notifications: {
merge(existing = [], incoming, { mergeObjects }) {
// Prepend new notifications
return [...incoming, ...existing];
},
},
},
},
},
});
```
## Field Policies
### read Function
Transform cached data when reading.
```typescript
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
// Computed field
fullName: {
read(_, { readField }) {
const firstName = readField("firstName");
const lastName = readField("lastName");
return `${firstName} ${lastName}`;
},
},
// Transform existing field
birthDate: {
read(existing) {
return existing ? new Date(existing) : null;
},
},
// Default value
role: {
read(existing = "USER") {
return existing;
},
},
},
},
},
});
```
### merge Function
Control how incoming data is stored.
```typescript
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
// Accumulate items instead of replacing
friends: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
// Merge objects deeply
settings: {
merge(existing, incoming, { mergeObjects }) {
return mergeObjects(existing, incoming);
},
},
},
},
Query: {
fields: {
// Merge paginated results
posts: {
keyArgs: ["category"], // Only category affects cache key
merge(existing = { items: [] }, incoming) {
return {
...incoming,
items: [...existing.items, ...incoming.items],
};
},
},
},
},
},
});
```
### keyArgs
Control which arguments affect cache storage.
```typescript
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Different cache entry per userId only
// (limit, offset don't create new entries)
userPosts: {
keyArgs: ["userId"],
},
// No arguments affect cache key
// (useful for pagination)
feed: {
keyArgs: false,
},
// Nested argument
search: {
keyArgs: ["filter", ["category", "status"]],
},
},
},
},
});
```
## Pagination
### Offset-Based Pagination
```typescript
import { offsetLimitPagination } from "@apollo/client/utilities";
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: offsetLimitPagination(),
// With key arguments
userPosts: offsetLimitPagination(["userId"]),
},
},
},
});
```
### Cursor-Based Pagination (Relay Style)
```typescript
import { relayStylePagination } from "@apollo/client/utilities";
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
posts: relayStylePagination(),
// With key arguments
userPosts: relayStylePagination(["userId"]),
},
},
},
});
```
### Custom Pagination
```typescript
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
feed: {
keyArgs: false,
merge(existing, incoming, { args }) {
const merged = existing ? existing.slice(0) : [];
const offset = args?.offset ?? 0;
for (let i = 0; i < incoming.length; i++) {
merged[offset + i] = incoming[i];
}
return merged;
},
read(existing, { args }) {
const offset = args?.offset ?? 0;
const limit = args?.limit ?? existing?.length ?? 0;
return existing?.slice(offset, offset + limit);
},
},
},
},
},
});
```
### fetchMore for Pagination
```tsx
function PostList() {
const { data, fetchMore, loading } = useQuery(GET_POSTS, {
variables: { offset: 0, limit: 10 },
});
const loadMore = () => {
fetchMore({
variables: {
offset: data.posts.length,
},
// With proper type policies, no updateQuery needed
});
};
return (
<div>
{data?.posts.map((post) => <PostCard key={post.id} post={post} />)}
<button onClick={loadMore} disabled={loading}>
Load More
</button>
</div>
);
}
```
## Cache Manipulation
### cache.readQuery
```typescript
// Read data from cache
const data = cache.readQuery({
query: GET_TODOS,
});
// With variables
const userData = cache.readQuery({
query: GET_USER,
variables: { id: "1" },
});
```
### cache.writeQuery
```typescript
// Write data to cache
cache.writeQuery({
query: GET_TODOS,
data: {
todos: [
{ __typename: "Todo", id: "1", text: "Buy milk", completed: false },
],
},
});
// With variables
cache.writeQuery({
query: GET_USER,
variables: { id: "1" },
data: {
user: { __typename: "User", id: "1", name: "John" },
},
});
```
### cache.readFragment / cache.writeFragment
```typescript
// Read a specific object - use cache.identify for safety
const user = cache.readFragment({
id: cache.identify({ __typename: "User", id: "1" }),
fragment: gql`
fragment UserFragment on User {
id
name
email
}
`,
});
// Apollo Client 4.1+: Use 'from' parameter (recommended)
const user = cache.readFragment({
from: { __typename: "User", id: "1" },
fragment: gql`
fragment UserFragment on User {
id
name
email
}
`,
});
// Update a specific object
cache.writeFragment({
id: cache.identify({ __typename: "User", id: "1" }),
fragment: gql`
fragment UpdateUser on User {
name
}
`,
data: {
name: "Jane",
},
});
// Apollo Client 4.1+: Use 'from' parameter (recommended)
cache.writeFragment({
from: { __typename: "User", id: "1" },
fragment: gql`
fragment UpdateUser on User {
name
}
`,
data: {
name: "Jane",
},
});
```
### cache.modify
```typescript
// Modify fields directly
cache.modify({
id: cache.identify(user),
fields: {
// Set new value
name: () => "New Name",
// Transform existing value
postCount: (existing) => existing + 1,
// Delete field
temporaryField: (_, { DELETE }) => DELETE,
// Add to array
friends: (existing, { toReference }) => [
...existing,
toReference({ __typename: "User", id: "2" }),
],
},
});
```
### cache.evict
```typescript
// Remove object from cache
cache.evict({ id: "User:1" });
// Remove specific field
cache.evict({ id: "User:1", fieldName: "friends" });
// Remove with broadcast (trigger re-renders)
cache.evict({ id: "User:1", broadcast: true });
```
## Garbage Collection
### Manual Garbage Collection
```typescript
// After evicting objects, clean up dangling references
cache.evict({ id: "User:1" });
cache.gc();
```
### Retaining Objects
```typescript
// Prevent objects from being garbage collected
const release = cache.retain("User:1");
// Later, allow GC
release();
cache.gc();
```
### Inspecting Cache
```typescript
// Get all cached data
const cacheContents = cache.extract();
// Restore cache state
cache.restore(previousCacheContents);
// Get identified object cache key
const userId = cache.identify({ __typename: "User", id: "1" });
// Returns: 'User:1'
```