HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•190 Minutes

GraphQL with React and Apollo Client

Consume GraphQL APIs from React using Apollo Client, understand queries, mutations, loading and error states, normalized caching, variables, refetching, and client-side data management.

Understanding Apollo Client

Apollo Client is a client-side GraphQL library that helps React applications execute operations and manage GraphQL data.

A GraphQL client is responsible for more than sending HTTP requests. Apollo Client can provide:

  • Query execution
  • Mutation execution
  • Request state management
  • Normalized caching
  • Cache updates
  • Optimistic UI
  • Polling
  • Refetching
  • Pagination helpers
  • Error handling

A useful mental model is:

React Component
      ↓
Apollo Client
      ↓
Cache ────────┐
      ↓       │
GraphQL Link │
      ↓       │
GraphQL API  │
      ↓       │
Response ────┘

Apollo Client can first satisfy a query from its cache depending on the configured fetch policy and then communicate with the server when required.

This is one of the major differences between using a raw fetch() call and using a full GraphQL client library.

Apollo Client Request Flow
Key Takeaways
  • Apollo Client is more than a GraphQL HTTP wrapper.
  • It can manage server data and a normalized client cache.
  • Fetch policies determine how cache and network are combined.
  • React components can subscribe to GraphQL query results.
Knowledge Check

Which capability distinguishes Apollo Client from simply using `fetch()` for GraphQL requests?

Setting Up Apollo Client in React

A React application typically creates an Apollo Client instance and provides it to the component tree.

Conceptually:

const client = new ApolloClient({
  uri: "/graphql",
  cache: new InMemoryCache()
});

The application then provides the client through Apollo's React integration.

<ApolloProvider client={client}>
  <App />
</ApolloProvider>

Components below the provider can use GraphQL hooks such as useQuery and useMutation.

The InMemoryCache is a normalized cache. It can store entities and references rather than simply caching entire query responses as opaque blobs.

In production applications, the Apollo Client setup often also includes an authentication link, error handling, retry behavior, upload support, tracing headers, or other transport concerns.

Keep transport configuration centralized instead of recreating clients inside individual React components.

Apollo Client Setup
import { ApolloClient, InMemoryCache } from "@apollo/client";

const client = new ApolloClient({
  uri: "/graphql",
  cache: new InMemoryCache()
});

export default client;
💡 The Apollo Client instance contains the GraphQL endpoint configuration and cache. It should generally be created once and provided to the application rather than instantiated repeatedly inside components.
Key Takeaways
  • Create a shared Apollo Client instance.
  • Use InMemoryCache for Apollo's normalized cache.
  • Provide the client to React through the Apollo provider.
  • Centralize authentication and transport configuration.
Knowledge Check

Why should an Apollo Client instance generally be shared rather than recreated for every React component?

Fetching GraphQL Data with useQuery

Apollo Client's useQuery hook connects a React component to a GraphQL query.

For example:

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

A component can use it:

const { data, loading, error } = useQuery(GET_USER, {
  variables: { id }
});

The component should account for at least three broad states:

Loading
   ↓
Success ─────→ Render data
   ↓
Error ───────→ Render error state

A production component should not assume data exists immediately.

For example:

if (loading) return <Spinner />;
if (error) return <ErrorMessage />;
if (!data?.user) return <NotFound />;

return <UserProfile user={data.user} />;

The exact UI depends on the application, but the important concept is that server state is asynchronous and can change independently of the component lifecycle.

React Query Component
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

function UserProfile({ id }) {
  const { data, loading, error } = useQuery(GET_USER, {
    variables: { id }
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Failed to load user.</p>;
  if (!data?.user) return <p>User not found.</p>;

  return <h1>{data.user.name}</h1>;
}
💡 The query declares an ID variable and requests the user fields required by the component. The component explicitly handles loading, error, missing-data, and success states.
Key Takeaways
  • useQuery connects a React component to a GraphQL query.
  • Variables are passed separately from the GraphQL document.
  • Components should handle loading and error states.
  • Server data should not be assumed to exist immediately.
Knowledge Check

Which state should a React component generally consider when using `useQuery`?

Apollo Fetch Policies

Apollo Client provides fetch policies that determine how a query interacts with the cache and network.

Common policies include:

cache-first

Use cached data when available. Otherwise request from the network.

network-only

Always request fresh data from the network and update the cache with the result.

cache-only

Read from the cache without making a network request.

no-cache

Request from the network without storing the result in the cache in the normal way.

cache-and-network

Return cached data when available while also requesting fresh network data.

The correct policy depends on the data.

For relatively stable reference data, cache-first can reduce unnecessary requests.

For rapidly changing data such as a live operational dashboard, network-only or cache-and-network may be more appropriate.

Do not choose network-only everywhere simply because freshness sounds safer. Doing so can throw away many of the benefits of client-side caching.

Conversely, do not use cache-first for data where stale information can create serious business problems without considering how and when the cache is invalidated.

Fetch Policies
cache-first: Cache first, network if missing
Good for data where cached results are useful and acceptable.
network-only: Always fetch from network
Useful when a fresh server result is required.
cache-and-network: Cache immediately and refresh from network
Useful when fast cached UI and eventual freshness are both desirable.
cache-only: Cache only
Useful when the application explicitly wants to avoid a network request.
no-cache: Network without normal cache storage
Useful when the result should not participate in normal cache storage.
Key Takeaways
  • Fetch policies control cache and network behavior.
  • No single fetch policy is correct for every query.
  • Freshness requirements should drive policy decisions.
  • Cache behavior and invalidation are part of application design.
Knowledge Check

Which fetch policy requests network data while also allowing cached data to be displayed first when available?

Executing Mutations from React

Apollo Client's mutation APIs allow React components to trigger GraphQL mutations.

For example:

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

The component can call:

const [createUser, { loading, error }] = useMutation(CREATE_USER);

Then:

await createUser({
  variables: {
    input: {
      name,
      email
    }
  }
});

After a mutation, the client cache may need to be updated.

Suppose the application has:

query Users {
  users {
    id
    name
  }
}

Creating a user does not necessarily mean Apollo can infer how your specific UI should update every query. Depending on the schema and cache configuration, you may need to use cache updates, refetching, or mutation result normalization.

A good client architecture treats mutation completion and cache consistency as explicit concerns.

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

function CreateUserForm() {
  const [createUser, { loading }] = useMutation(CREATE_USER);

  const submit = async () => {
    await createUser({
      variables: {
        input: {
          name: "Alice",
          email: "alice@example.com"
        }
      }
    });
  };

  return <button disabled={loading} onClick={submit}>Create</button>;
}
💡 The mutation hook returns a function used to execute the operation and state describing the mutation. After completion, the application may need to update or refetch related cached queries.
Key Takeaways
  • Mutations are triggered explicitly by application actions.
  • Mutation variables are supplied separately.
  • Mutation completion may require cache updates.
  • Loading and error states should be represented in the UI.
Knowledge Check

Why might a React application need to update the Apollo cache after a successful mutation?