All skills
apollographql avatar

/apollo-client

@cb48e40 official
by Apollo GraphQLapollographql/skills115 stars
13

Guide for building React applications with Apollo Client 4.x. Use this skill when: (1) setting up Apollo Client in a React project, (2) writing GraphQL queries or mutations with hooks, (3) configuring caching or cache policies, (4) managing local state with reactive variables, (5) troubleshooting Apollo Client errors or performance issues.

Use this Skill: https://skilld.dev/gh/apollographql/skills/apollo-client

This session only. Nothing lands on disk.

referencescaching.md

≈2.7k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Caching Reference

Table of Contents

InMemoryCache Setup

Basic Configuration

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

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

# Query
query GetPost {
  post(id: "1") {
    id
    title
    author {
      id
      name
    }
  }
}
// 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.

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.

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.

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.

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.

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

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)

import { relayStylePagination } from "@apollo/client/utilities";

const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        posts: relayStylePagination(),

        // With key arguments
        userPosts: relayStylePagination(["userId"]),
      },
    },
  },
});

Custom Pagination

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

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

// 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

// 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

// 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

// 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

// 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

// After evicting objects, clean up dangling references
cache.evict({ id: "User:1" });
cache.gc();

Retaining Objects

// Prevent objects from being garbage collected
const release = cache.retain("User:1");

// Later, allow GC
release();
cache.gc();

Inspecting Cache

// 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'

Source: SKILL.md on GitHub

1 warning16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill is a comprehensive reference and integration guide for Apollo Client 4.x in React applications. No security vulnerabilities, malicious code, or adversarial patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    4/14 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at cb48e40. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 5 months ago
What it can do
Runs commands
metadata
{
  "author": "apollographql",
  "version": "1.0.0"
}
All 1 allowed tools
Bash(npm:*) Bash(npx:*) Bash(node:*) Read Write Edit Glob Grep
Other metadata
compatibility
React 18+, React 19 (Suspense/RSC). Works with Next.js, Vite, CRA, and other React frameworks.
  • React
  • TypeScript
  • apollo-client
  • graphql
  • state-management
  • caching
  • queries
  • mutations
  • suspense

README badge

README badge for apollographql/skills/apollo-client

Guides for building React applications with Apollo Client 4.x, covering queries, mutations, caching, local state, and Suspense integration. Targets React 18+, React 19, Next.js, Vite, and other React frameworks with patterns for client-side apps, server-side rendering, and modern data fetching.

Generated from the current SKILL.md.

Does this skill work with React Server Components and Next.js App Router?
Yes. The skill includes integration guides for Next.js App Router with React Server Components, and supports React 19 with Suspense.
What Apollo Client version does this skill target?
Apollo Client 4.x specifically. The skill does not cover v3 or earlier patterns.
Does this skill cover TypeScript code generation for GraphQL operations?
Yes. The skill includes a reference section on TypeScript Code Generator setup for type-safe operations.
Can I use this skill to configure caching and optimize performance?
Yes. The skill covers InMemoryCache configuration, typePolicies, fetchPolicy strategies, and performance patterns including @defer and @stream.
What frameworks does this skill support beyond client-side React?
It includes integration guides for Next.js App Router, React Router 7 with streaming SSR, and TanStack Start, in addition to client-side apps built with Vite or Create React App.

Generated from the current SKILL.md. These answers refresh after source changes.