Apollo Client Integration for Client-Side Apps
This guide covers setting up Apollo Client in client-side React applications without server-side rendering (SSR). This includes applications using Vite, Parcel, Create React App, or other bundlers that don't implement SSR.
For applications with SSR, use one of the framework-specific integration guides instead:
Installation
npm install @apollo/client graphql rxjsTypeScript Code Generation (optional but recommended)
For type-safe GraphQL operations with TypeScript, see the TypeScript Code Generation guide.
Setup Steps
Step 1: Create Client
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
// Recommended: Use HttpOnly cookies for authentication
const httpLink = new HttpLink({
uri: "https://your-graphql-endpoint.com/graphql",
credentials: "include", // Sends cookies with requests (secure when using HttpOnly cookies)
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
});If you need manual token management (less secure, only when HttpOnly cookies aren't available):
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
import { SetContextLink } from "@apollo/client/link/context";
const httpLink = new HttpLink({
uri: "https://your-graphql-endpoint.com/graphql",
});
const authLink = new SetContextLink(({ headers }) => {
const token = localStorage.getItem("token");
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});Step 2: Setup Provider
import { ApolloProvider } from "@apollo/client";
import App from "./App";
function Root() {
return (
<ApolloProvider client={client}>
<App />
</ApolloProvider>
);
}Step 3: Execute Query
import { gql } from "@apollo/client";
import { useQuery } from "@apollo/client/react";
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
function UserList() {
const { loading, error, data, dataState } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
// TypeScript note: for stricter type narrowing, you can also check `dataState === "complete"` before accessing data
return (
<ul>{data?.users.map((user) => <li key={user.id}>{user.name}</li>)}</ul>
);
}Basic Query Usage
Using Variables
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ userId }: { userId: string }) {
const { loading, error, data, dataState } = useQuery(GET_USER, {
variables: { id: userId },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
// TypeScript note: for stricter type narrowing, you can also check `dataState === "complete"` before accessing data
return <div>{data?.user.name}</div>;
}Note for TypeScript users: Use
dataStatefor more robust type safety and better type narrowing in Apollo Client 4.x.
TypeScript Integration
For complete examples with loading, error handling, and dataState for type narrowing, see Basic Query Usage above.
Usage with Generated Types
For type-safe operations with code generation, see the TypeScript Code Generation guide.
Quick example:
import { gql } from "@apollo/client";
import { useQuery } from "@apollo/client/react";
import { GetUserDocument } from "./queries.generated";
// Define your query with the if (false) pattern for code generation
if (false) {
gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
}
function UserProfile({ userId }: { userId: string }) {
// Types are automatically inferred from GetUserDocument
const { data } = useQuery(GetUserDocument, {
variables: { id: userId },
});
return <div>{data.user.name}</div>;
}Usage with Manual Type Annotations
If not using code generation, define types alongside your queries using TypedDocumentNode:
import { gql, TypedDocumentNode } from "@apollo/client";
import { useQuery } from "@apollo/client/react";
interface GetUserData {
user: {
id: string;
name: string;
email: string;
};
}
interface GetUserVariables {
id: string;
}
// Types should always be defined via TypedDocumentNode alongside your queries/mutations, not at the useQuery/useMutation call site
const GET_USER: TypedDocumentNode<GetUserData, GetUserVariables> = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
function UserProfile({ userId }: { userId: string }) {
const { data } = useQuery(GET_USER, {
variables: { id: userId },
});
// data.user is automatically typed from GET_USER
return <div>{data.user.name}</div>;
}Basic Mutation Usage
import { gql, TypedDocumentNode } from "@apollo/client";
import { useMutation } from "@apollo/client/react";
interface CreateUserMutation {
createUser: {
id: string;
name: string;
email: string;
};
}
interface CreateUserMutationVariables {
input: {
name: string;
email: string;
};
}
const CREATE_USER: TypedDocumentNode<
CreateUserMutation,
CreateUserMutationVariables
> = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
function CreateUserForm() {
const [createUser, { loading, error }] = useMutation(CREATE_USER);
const handleSubmit = async (formData: FormData) => {
const { data } = await createUser({
variables: {
input: {
name: formData.get("name") as string,
email: formData.get("email") as string,
},
},
});
if (data) {
console.log("Created user:", data.createUser);
}
};
return (
<form
onSubmit={(e) => {
e.preventDefault();
handleSubmit(new FormData(e.currentTarget));
}}
>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" disabled={loading}>
{loading ? "Creating..." : "Create User"}
</button>
{error && <p>Error: {error.message}</p>}
</form>
);
}Client Configuration Options
const client = new ApolloClient({
// Required: The cache implementation
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Field-level cache configuration
},
},
},
}),
// Network layer
link: new HttpLink({ uri: "/graphql" }),
// Avoid defaultOptions if possible as they break TypeScript expectations.
// Configure options per-query/mutation instead for better type safety.
// defaultOptions: {
// watchQuery: { fetchPolicy: 'cache-and-network' },
// },
// DevTools are enabled by default in development
// Only configure when enabling in production
devtools: {
enabled: true, // Only needed for production
},
// Custom name for this client instance
clientAwareness: {
name: "web-client",
version: "1.0.0",
},
});Important Considerations
Choose Your Hook Strategy: Decide if your application should be based on Suspense. If it is, use suspenseful hooks like
useSuspenseQuery(see Suspense Hooks guide), otherwise use non-suspenseful hooks likeuseQuery(see Queries guide).Client-Side Only: This setup is for client-side apps without SSR. The Apollo Client instance is created once and reused throughout the application lifecycle.
Authentication: Prefer HttpOnly cookies with
credentials: "include"inHttpLinkoptions to avoid exposing tokens to JavaScript. If manual token management is necessary, useSetContextLinkto dynamically add authentication headers fromlocalStorageor other client-side storage.Environment Variables: Store your GraphQL endpoint URL in environment variables for different environments (development, staging, production).
Error Handling: Always handle
loadinganderrorstates when usinguseQueryoruseLazyQuery. For Suspense-based hooks (useSuspenseQuery), React handles this through<Suspense>boundaries and error boundaries.