Intelligent caching with normalization, reactive updates, and flexible policies. The Apollo Client caching system provides automatic normalization, reactive variable support, and comprehensive cache management for GraphQL data with intelligent updates and query result optimization.
The main cache implementation providing normalized storage with reactive updates and flexible configuration.
/**
* In-memory cache with automatic normalization and reactive updates
* Stores GraphQL data in a normalized format with intelligent merging
*/
class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
constructor(config?: InMemoryCacheConfig);
/** Type policies for controlling cache behavior */
policies: Policies;
}
interface InMemoryCacheConfig {
/** Custom data ID generation function */
dataIdFromObject?: KeyFieldsFunction;
/** Add __typename to all objects */
addTypename?: boolean;
/** Type policies for cache behavior */
typePolicies?: TypePolicies;
/** Possible types for union/interface types */
possibleTypes?: PossibleTypesMap;
/** Result caching configuration */
resultCaching?: boolean;
/** Canonical cache key computation */
canonizeResults?: boolean;
}Usage Example:
import { InMemoryCache } from "@apollo/client/cache";
const cache = new InMemoryCache({
typePolicies: {
User: {
fields: {
posts: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
},
Post: {
keyFields: ["slug"]
}
},
possibleTypes: {
Node: ["User", "Post", "Comment"]
}
});Read data from the cache using queries or fragments.
/**
* Read query data from the cache
* @param options - Read options with query and variables
* @returns Query result or null if not cached
*/
readQuery<QueryType, TVariables = any>(
options: Cache.ReadQueryOptions<QueryType, TVariables>
): QueryType | null;
/**
* Read fragment data from the cache
* @param options - Fragment read options with ID and fragment
* @returns Fragment data or null if not found
*/
readFragment<FragmentType, TVariables = any>(
options: Cache.ReadFragmentOptions<FragmentType, TVariables>
): FragmentType | null;
interface Cache.ReadQueryOptions<TQuery, TVariables> {
query: DocumentNode | TypedDocumentNode<TQuery, TVariables>;
variables?: TVariables;
rootId?: string;
previousResult?: any;
returnPartialData?: boolean;
optimistic?: boolean;
}
interface Cache.ReadFragmentOptions<TFragment, TVariables> {
id: string;
fragment: DocumentNode | TypedDocumentNode<TFragment, TVariables>;
variables?: TVariables;
returnPartialData?: boolean;
optimistic?: boolean;
}Usage Example:
// Read query from cache
const userData = cache.readQuery({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '123' }
});
// Read fragment from cache
const userFragment = cache.readFragment({
id: 'User:123',
fragment: gql`
fragment UserInfo on User {
name
email
avatar
}
`
});Write data to the cache using queries or fragments.
/**
* Write query data to the cache
* @param options - Write options with query, variables, and data
*/
writeQuery<TData, TVariables = any>(
options: Cache.WriteQueryOptions<TData, TVariables>
): void;
/**
* Write fragment data to the cache
* @param options - Fragment write options
* @returns Reference to the written object
*/
writeFragment<TData, TVariables = any>(
options: Cache.WriteFragmentOptions<TData, TVariables>
): Reference | undefined;
interface Cache.WriteQueryOptions<TData, TVariables> {
query: DocumentNode | TypedDocumentNode<TData, TVariables>;
variables?: TVariables;
data: TData;
id?: string;
broadcast?: boolean;
}
interface Cache.WriteFragmentOptions<TData, TVariables> {
id: string;
fragment: DocumentNode | TypedDocumentNode<TData, TVariables>;
variables?: TVariables;
data: TData;
broadcast?: boolean;
}Usage Example:
// Write query to cache
cache.writeQuery({
query: GET_USER,
variables: { id: '123' },
data: {
user: {
id: '123',
name: 'John Doe',
email: 'john@example.com',
__typename: 'User'
}
}
});
// Write fragment to cache
const ref = cache.writeFragment({
id: 'User:123',
fragment: gql`
fragment UserInfo on User {
name
email
}
`,
data: {
name: 'John Doe Updated',
email: 'john.updated@example.com',
__typename: 'User'
}
});Modify cached data with fine-grained control using the modify method.
/**
* Modify cached fields with granular control
* @param options - Modification options with ID and field modifiers
* @returns Boolean indicating if any modifications were made
*/
modify(options: Cache.ModifyOptions): boolean;
interface Cache.ModifyOptions {
/** ID of the object to modify */
id?: string;
/** Field modification functions */
fields?: Modifiers | Modifier<any>;
/** Whether to broadcast changes */
broadcast?: boolean;
/** Optimistic modification */
optimistic?: boolean;
}
type Modifiers = {
[fieldName: string]: Modifier<any>;
};
type Modifier<T> = (value: T, details: ModifierDetails) => T | typeof INVALIDATE | typeof DELETE;
interface ModifierDetails {
/** Field name being modified */
fieldName: string;
/** Storage key for the field */
storeFieldName: string;
/** Arguments for the field */
args?: Record<string, any>;
/** Check if value is a cache reference */
isReference: (value: any) => value is Reference;
/** Convert cache reference to object */
toReference: typeof toReference;
/** Read field from another object */
readField: ReadFieldFunction;
}Usage Example:
// Add item to a list
cache.modify({
id: 'User:123',
fields: {
posts(existingPosts = [], { readField }) {
const newPost = cache.writeFragment({
data: {
id: 'post-456',
title: 'New Post',
__typename: 'Post'
},
fragment: gql`
fragment NewPost on Post {
id
title
}
`
});
return [...existingPosts, newPost];
},
postCount(existing) {
return existing + 1;
}
}
});
// Remove item from cache
cache.modify({
id: 'ROOT_QUERY',
fields: {
posts(existingPosts, { readField }) {
return existingPosts.filter(
post => readField('id', post) !== 'post-to-remove'
);
}
}
});High-level cache management operations for resetting, garbage collection, and state inspection.
/**
* Reset the cache, clearing all data
* @returns Promise that resolves when reset is complete
*/
reset(): Promise<void>;
/**
* Evict objects or fields from the cache
* @param options - Eviction options
* @returns Boolean indicating if anything was evicted
*/
evict(options: Cache.EvictOptions): boolean;
/**
* Garbage collect unreferenced objects
* @returns Array of IDs that were collected
*/
gc(): string[];
/**
* Retain a cache object to prevent garbage collection
* @param id - Object ID to retain
* @returns Release function
*/
retain(id: string): number;
/**
* Release a retained object
* @param id - Object ID to release
* @returns Remaining retain count
*/
release(id: string): number;
/**
* Extract the entire cache state
* @param optimistic - Include optimistic data
* @returns Complete cache state
*/
extract(optimistic?: boolean): NormalizedCacheObject;
/**
* Restore cache state from extracted data
* @param serializedState - Previously extracted cache state
*/
restore(serializedState: NormalizedCacheObject): this;
interface Cache.EvictOptions {
/** ID of object to evict from */
id?: string;
/** Specific field name to evict */
fieldName?: string;
/** Arguments for the field to evict */
args?: Record<string, any>;
/** Broadcast the eviction */
broadcast?: boolean;
}Usage Example:
// Evict specific object
cache.evict({ id: 'User:123' });
// Evict specific field
cache.evict({
id: 'User:123',
fieldName: 'posts',
args: { limit: 10 }
});
// Garbage collect unreferenced objects
const collected = cache.gc();
console.log(`Collected ${collected.length} objects`);
// Extract and restore cache
const state = cache.extract();
const newCache = new InMemoryCache();
newCache.restore(state);Create reactive variables for local state management outside the GraphQL schema.
/**
* Create a reactive variable for local state
* @param initialValue - Initial value for the variable
* @returns Reactive variable function
*/
function makeVar<T>(initialValue: T): ReactiveVar<T>;
interface ReactiveVar<T> {
/** Get current value */
(): T;
/** Set new value */
(newValue: T): T;
/** Listen for changes (internal use) */
onNextChange(listener: (value: T) => void): () => void;
}Usage Example:
import { makeVar } from "@apollo/client/cache";
// Create reactive variables
const isLoggedInVar = makeVar(false);
const currentUserVar = makeVar(null);
const themeVar = makeVar('light');
// Use in components (with useReactiveVar)
function LoginStatus() {
const isLoggedIn = useReactiveVar(isLoggedInVar);
return (
<div>
Status: {isLoggedIn ? 'Logged In' : 'Logged Out'}
<button onClick={() => isLoggedInVar(!isLoggedIn)}>
Toggle Status
</button>
</div>
);
}
// Use in type policies
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
isLoggedIn: {
read() {
return isLoggedInVar();
}
},
currentUser: {
read() {
return currentUserVar();
}
}
}
}
}
});Configure cache behavior for specific GraphQL types and fields.
interface TypePolicies {
[typename: string]: TypePolicy;
}
interface TypePolicy {
/** Fields used to identify objects of this type */
keyFields?: KeySpecifier | KeyFieldsFunction | false;
/** Field-specific policies */
fields?: {
[fieldName: string]: FieldPolicy<any>;
};
/** Custom merge function for this type */
merge?: FieldMergeFunction<any, any>;
}
interface FieldPolicy<TExisting = any, TIncoming = TExisting, TReadResult = TIncoming> {
/** Custom field read function */
read?: FieldReadFunction<TExisting, TReadResult>;
/** Custom field merge function */
merge?: FieldMergeFunction<TExisting, TIncoming>;
/** Key arguments that affect field identity */
keyArgs?: KeySpecifier | KeyArgsFunction | false;
}
type FieldReadFunction<TExisting, TReadResult = TExisting> = (
existing: TExisting | undefined,
options: FieldFunctionOptions
) => TReadResult;
type FieldMergeFunction<TExisting, TIncoming = TExisting> = (
existing: TExisting | undefined,
incoming: TIncoming,
options: FieldFunctionOptions
) => TExisting | TIncoming;Usage Example:
const cache = new InMemoryCache({
typePolicies: {
User: {
keyFields: ["email"], // Use email instead of id
fields: {
posts: {
merge(existing = [], incoming) {
// Merge arrays without duplicates
const existingIds = existing.map(ref => readField('id', ref));
return [
...existing,
...incoming.filter(ref =>
!existingIds.includes(readField('id', ref))
)
];
}
},
fullName: {
read(_, { readField }) {
// Computed field
const firstName = readField('firstName');
const lastName = readField('lastName');
return `${firstName} ${lastName}`;
}
}
}
},
Post: {
keyFields: ["slug"],
fields: {
comments: relayStylePagination() // Use built-in pagination
}
}
}
});interface NormalizedCacheObject {
[dataId: string]: StoreObject | undefined;
}
interface StoreObject {
__typename?: string;
[storeFieldName: string]: StoreValue;
}
type StoreValue =
| number
| string
| boolean
| null
| undefined
| StoreObject
| StoreObject[]
| Reference
| Reference[];
interface Reference {
readonly __ref: string;
}
type KeySpecifier = (string | KeySpecifier)[];
type KeyFieldsFunction = (object: Readonly<StoreObject>, context: {
typename: string;
selectionSet?: SelectionSetNode;
fragmentMap?: FragmentMap;
}) => string | null | void;
interface PossibleTypesMap {
[supertype: string]: string[];
}
interface FieldFunctionOptions {
args?: Record<string, any> | null;
fieldName: string;
storeFieldName: string;
field?: FieldNode;
typename?: string;
cache: InMemoryCache;
canRead: CanReadFunction;
readField: ReadFieldFunction;
mergeObjects: MergeObjectsFunction;
variables?: Record<string, any>;
isReference: (obj: any) => obj is Reference;
toReference: ToReferenceFunction;
storage: StorageType;
}