Apollo 16: Custom Hooks
easy⏱ 5 mincourseapollo
Why wrap Apollo hooks?
Raw useQuery and useMutation calls scattered across components create duplication and tight coupling to your GraphQL schema. Custom hooks solve this by:
- Encapsulating the query, variables, and options in one place.
- Adding type safety — the hook returns strongly-typed data, not
any.
- Abstracting complexity — pagination logic, optimistic updates, and error handling live inside the hook, not in the component.
- Enabling testing — mock the custom hook instead of MockedProvider.
The pattern is simple: define a function that calls Apollo hooks internally and returns a curated API.
// Before: duplicated everywhere
const { data } = useQuery(GET_CURRENT_USER);
const user = data?.currentUser;
// After: one clean hook
function useCurrentUser() {
const { data, loading, error } = useQuery(GET_CURRENT_USER);
return {
user: data?.currentUser ?? null,
loading,
error,
isAuthenticated: !!data?.currentUser,
};
}
// Usage
const { user, isAuthenticated } = useCurrentUser();
Create useCurrentUser
Define a GET_CURRENT_USER query that fetches id, name, email, avatar, and role. Create a useCurrentUser custom hook that calls useQuery(GET_CURRENT_USER) internally and returns { user, loading, error, isAuthenticated, isAdmin }. Derive isAuthenticated from !!data?.currentUser and isAdmin from user?.role === 'ADMIN'. Use the hook in a Navbar component to show the user's name or a login button.
Typing custom hooks with generics
Use TypeScript generics to create reusable custom hooks. For example, usePaginatedQuery<TData, TVars> accepts any query and returns paginated data with loadMore and hasMore. The key is to constrain the generic with DocumentNode and use Apollo's TypedDocumentNode for end-to-end type inference: function usePaginatedQuery<TData, TVars>(query: TypedDocumentNode<TData, TVars>, options: QueryHookOptions<TData, TVars>).
import { TypedDocumentNode } from '@apollo/client';
function usePaginatedQuery<TData, TVars extends { offset: number; limit: number }>(
query: TypedDocumentNode<TData, TVars>,
options: { variables: TVars; getItems: (data: TData) => unknown[] }
) {
const { data, loading, fetchMore } = useQuery(query, {
variables: options.variables,
});
const items = data ? options.getItems(data) : [];
const loadMore = () =>
fetchMore({
variables: { ...options.variables, offset: items.length } as TVars,
});
return { items, loading, loadMore, hasMore: items.length % options.variables.limit === 0 };
}
Hook composition patterns
Custom hooks compose — build small hooks that combine into larger ones:
useCurrentUser() — fetches and returns the current user.
usePermissions(user) — derives permissions from the user object.
useAuthGuard() — calls both, redirects if not authenticated.
Each hook has a single responsibility. The composition happens at the call site, not inside a God hook. This mirrors React's philosophy: prefer composition over inheritance, even in data-fetching.