Skip to content

Apollo Client Reference: useQuery, useMutation, Cache, Subscriptions & Codegen

Apollo Client is the most widely-used GraphQL client for JavaScript. It handles data fetching, caching, and state management for GraphQL queries. The killer feature is the normalized in-memory cache: results are stored by type + ID, so updating one object updates it everywhere it’s referenced in your UI without any extra work. For simple read-heavy apps you can replace most of your state management with Apollo queries.

1. Setup & useQuery

ApolloClient setup, InMemoryCache, ApolloProvider, and the useQuery hook
// npm install @apollo/client graphql

// client.ts — configure the Apollo client:
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";

const httpLink = createHttpLink({ uri: "https://api.example.com/graphql" });

// Add auth header to every request:
const authLink = setContext((_, { headers }) => ({
  headers: { ...headers, authorization: `Bearer ${localStorage.getItem("token")}` }
}));

export const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
});

// Wrap your app:
import { ApolloProvider } from "@apollo/client";
root.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

// useQuery — fetch data declaratively:
import { useQuery, gql } from "@apollo/client";

const GET_USERS = gql`
  query GetUsers($limit: Int) {
    users(limit: $limit) {
      id
      name
      email
      role
    }
  }
`;

function UserList() {
  const { loading, error, data } = useQuery(GET_USERS, {
    variables: { limit: 20 },
    // fetchPolicy: "cache-first"    (default — check cache first)
    // fetchPolicy: "network-only"   (skip cache, always fetch)
    // fetchPolicy: "cache-and-network" (show cached instantly, update in background)
    pollInterval: 30000,  // refetch every 30s
  });

  if (loading) return <Spinner />;
  if (error) return <p>Error: {error.message}</p>;
  return <ul>{data.users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

2. useMutation & Cache Updates

useMutation, optimistic updates, and manually updating the cache after mutation
import { useMutation, gql } from "@apollo/client";

const CREATE_USER = gql`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER, {
    // Option 1: refetch queries after mutation:
    refetchQueries: [{ query: GET_USERS, variables: { limit: 20 } }],

    // Option 2: manually update cache (faster — no extra network request):
    update(cache, { data: { createUser } }) {
      cache.modify({
        fields: {
          users(existingUsers = []) {
            const newRef = cache.writeFragment({
              data: createUser,
              fragment: gql`fragment NewUser on User { id name email }`,
            });
            return [...existingUsers, newRef];
          },
        },
      });
    },

    // Option 3: optimistic response (show result before server confirms):
    optimisticResponse: {
      createUser: { __typename: "User", id: "temp-id", name: "Alice", email: "alice@example.com" },
    },
  });

  const handleSubmit = async (values) => {
    try {
      const { data } = await createUser({ variables: { input: values } });
      console.log("Created:", data.createUser);
    } catch (e) {
      console.error("Mutation failed:", e.message);
    }
  };

  return <form onSubmit={handleSubmit}>...</form>;
}

// Delete and evict from cache:
const DELETE_USER = gql`mutation DeleteUser($id: ID!) { deleteUser(id: $id) { id } }`;

const [deleteUser] = useMutation(DELETE_USER, {
  update(cache, { data: { deleteUser } }) {
    cache.evict({ id: cache.identify(deleteUser) });  // remove by type+id
    cache.gc();  // garbage collect unreachable objects
  },
});

3. Cache Normalization & Policies

How InMemoryCache normalizes data, keyFields, typePolicies, and field merge functions
// InMemoryCache normalizes by __typename + id by default
// Every User with id "123" is stored once at User:123
// All queries that returned that user reference the SAME cache entry
// Update it once → all components re-render with new data

// typePolicies — customize cache key or field behavior:
const cache = new InMemoryCache({
  typePolicies: {
    User: {
      keyFields: ["email"],     // use email as unique key instead of id
    },
    Query: {
      fields: {
        // Paginated field — merge pages together instead of replacing:
        users: {
          keyArgs: ["filter"],   // only these args matter for cache key
          merge(existing = [], incoming) {
            return [...existing, ...incoming];  // append new page
          },
          read(existing) {
            return existing;   // always read from merged array
          },
        },
      },
    },
  },
});

// Read directly from cache (no network):
const user = client.readFragment({
  id: "User:123",
  fragment: gql`fragment UserFields on User { id name email }`,
});

// Write directly to cache (optimistic UI, offline, tests):
client.writeFragment({
  id: "User:123",
  fragment: gql`fragment UserFields on User { id name email }`,
  data: { id: "123", name: "Alice Updated", email: "alice@example.com" },
});

// Invalidate + refetch specific queries:
client.refetchQueries({ include: [GET_USERS] });

// Clear entire cache:
client.clearStore();           // clears and resets active queries
client.resetStore();           // clears and refetches active queries

4. useSubscription, Lazy Queries & Error Handling

WebSocket subscriptions, useLazyQuery for on-demand fetching, and error policies
// WebSocket for subscriptions:
// npm install @apollo/client subscriptions-transport-ws graphql-ws

import { split } from "@apollo/client";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient } from "graphql-ws";
import { getMainDefinition } from "@apollo/client/utilities";

const wsLink = new GraphQLWsLink(createClient({ url: "ws://localhost:4000/graphql" }));

// Route subscriptions over WebSocket, queries/mutations over HTTP:
const splitLink = split(
  ({ query }) => {
    const { kind, operation } = getMainDefinition(query) as any;
    return kind === "OperationDefinition" && operation === "subscription";
  },
  wsLink,
  authLink.concat(httpLink),
);
const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache() });

// useSubscription:
const MESSAGES_SUB = gql`subscription { messageSent { id text author { name } } }`;
const { data: sub } = useSubscription(MESSAGES_SUB, {
  onData: ({ data }) => console.log("New message:", data.data.messageSent),
});

// useLazyQuery — execute on demand (e.g., on button click, not on mount):
const [searchUsers, { loading, data }] = useLazyQuery(SEARCH_USERS);
<button onClick={() => searchUsers({ variables: { query: "alice" } })}>Search</button>

// Error handling — separate network errors from GraphQL errors:
const { error } = useQuery(GET_USERS);
if (error?.networkError) return <p>Network error: {error.networkError.message}</p>;
if (error?.graphQLErrors.length) return <p>{error.graphQLErrors[0].message}</p>;

5. TypeScript Code Generation & Apollo Server

graphql-codegen for typed operations, and Apollo Server 4 basics
// graphql-codegen — generate TypeScript types from schema:
// npm install -D @graphql-codegen/cli @graphql-codegen/typescript
//                @graphql-codegen/typescript-operations
//                @graphql-codegen/typescript-react-apollo

// codegen.ts:
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
  schema: "http://localhost:4000/graphql",   // or local .graphql files
  documents: ["src/**/*.tsx", "src/**/*.ts"],
  generates: {
    "./src/generated/graphql.ts": {
      plugins: ["typescript", "typescript-operations", "typescript-react-apollo"],
      config: { withHooks: true },
    },
  },
};
export default config;

// After running npx graphql-codegen, use typed hooks:
// BEFORE (untyped):
const { data } = useQuery(GET_USERS);
data.users[0].name;   // TypeScript doesn't know the shape

// AFTER (fully typed):
import { useGetUsersQuery } from "./generated/graphql";
const { data } = useGetUsersQuery({ variables: { limit: 20 } });
data?.users[0].name;  // TypeScript knows every field

// Apollo Server 4 basics (backend):
// npm install @apollo/server graphql
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const server = new ApolloServer({
  typeDefs: `
    type User { id: ID!; name: String!; email: String! }
    type Query { users: [User!]! }
  `,
  resolvers: {
    Query: { users: () => db.users.findAll() },
  },
});
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
// Or with Express: expressMiddleware(server) for existing Express app

Track Node.js and GraphQL releases at ReleaseRun. Related: GraphQL Reference | tRPC Reference | TanStack Query Reference

🔍 Free tool: npm Package Health Checker — check Apollo Client and related GraphQL packages for known CVEs and active maintenance.

Founded

2023 in London, UK

Contact

hello@releaserun.com