GraphQL Fragments
Fragments are reusable units of query logic in GraphQL. They let you define a set of fields once and include them in multiple queries or mutations. This reduces duplication and ensures consistency — when you need to change which fields are fetched for a type, you update the fragment in one place.
Defining and Using Fragments
# Define a fragment on the User type
fragment UserBasicInfo on User {
id
name
email
avatarUrl
}
fragment PostSummary on Post {
id
title
excerpt
publishedAt
likesCount
author {
...UserBasicInfo
}
}
fragment PostFull on Post {
...PostSummary
body
tags {
id
name
}
comments {
id
text
author {
...UserBasicInfo
}
createdAt
}
}
# Use fragments in queries
query GetDashboard {
currentUser {
...UserBasicInfo
role
unreadNotificationsCount
}
featuredPosts: posts(limit: 3, featured: true) {
...PostSummary
}
recentPosts: posts(limit: 10) {
...PostSummary
}
}
query GetPostDetail($postId: ID!) {
post(id: $postId) {
...PostFull
}
}
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
post {
...PostFull
}
}
}
Benefits of Fragments
- DRY (Don't Repeat Yourself): Define field selections once, use them everywhere
- Consistency: All queries that use a fragment get the same fields
- Maintainability: Adding a field to a fragment updates all queries that use it
- Co-location: Components can define their own data requirements as fragments
Inline Fragments
Inline fragments are used to query fields on specific types within a union or interface. They do not need a name and are written directly in the query:
# Union type requires inline fragments
union SearchResult = User | Post | Comment
query Search($query: String!) {
search(query: $query) {
# Common field available on all types (if using an interface)
__typename
... on User {
id
name
avatarUrl
bio
}
... on Post {
id
title
excerpt
author { name }
}
... on Comment {
id
text
post { title }
author { name }
}
}
}
# Inline fragments can also be used for conditional field selection
query GetUser($id: ID!, $includeEmail: Boolean!) {
user(id: $id) {
id
name
... @include(if: $includeEmail) {
email
emailVerified
}
}
}
Fragment Co-location Pattern in React
A powerful pattern in React applications is co-locating GraphQL fragments with the components that use them. Each component declares its own data requirements:
// UserAvatar.tsx — component declares its data needs
import { gql } from '@apollo/client';
export const USER_AVATAR_FRAGMENT = gql`
fragment UserAvatar on User {
id
name
avatarUrl
}
`;
interface UserAvatarProps {
user: {
id: string;
name: string;
avatarUrl: string | null;
};
}
export function UserAvatar({ user }: UserAvatarProps) {
return (
<div className="flex items-center gap-2">
<img
src={user.avatarUrl || '/default-avatar.png'}
alt={user.name}
className="w-8 h-8 rounded-full"
/>
<span className="font-medium">{user.name}</span>
</div>
);
}
// PostCard.tsx — composes fragments from child components
import { gql } from '@apollo/client';
import { USER_AVATAR_FRAGMENT, UserAvatar } from './UserAvatar';
export const POST_CARD_FRAGMENT = gql`
fragment PostCard on Post {
id
title
excerpt
publishedAt
likesCount
author {
...UserAvatar
}
}
${USER_AVATAR_FRAGMENT}
`;
export function PostCard({ post }: { post: PostCardFragment }) {
return (
<article className="border rounded-lg p-4">
<UserAvatar user={post.author} />
<h2 className="text-xl font-bold mt-2">{post.title}</h2>
<p className="text-gray-600">{post.excerpt}</p>
<div className="flex gap-4 mt-3 text-sm text-gray-500">
<span>{post.likesCount} likes</span>
<span>{post.publishedAt}</span>
</div>
</article>
);
}
// PostList.tsx — page query composes all fragments
import { gql, useQuery } from '@apollo/client';
import { POST_CARD_FRAGMENT, PostCard } from './PostCard';
const GET_POSTS = gql`
query GetPosts($limit: Int!, $offset: Int!) {
posts(limit: $limit, offset: $offset) {
...PostCard
}
}
${POST_CARD_FRAGMENT}
`;
export function PostList() {
const { data, loading } = useQuery(GET_POSTS, {
variables: { limit: 10, offset: 0 },
});
if (loading) return <div>Loading...</div>;
return (
<div className="space-y-4">
{data.posts.map((post: any) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
Variables Deep Dive
Variables make GraphQL operations dynamic and reusable. They separate the static query structure from the dynamic values, which is critical for security (preventing injection) and caching:
# Variable types match schema types
query GetPosts(
$limit: Int! = 20 # Required with default
$offset: Int = 0 # Optional with default
$status: PostStatus # Optional enum
$authorId: ID # Optional
$tags: [String!] # Optional list
$search: String # Optional
) {
posts(
limit: $limit
offset: $offset
filter: {
status: $status
authorId: $authorId
tags: $tags
search: $search
}
) {
id
title
status
tags { name }
}
}
# Directives with variables for conditional fields
query GetUser($id: ID!, $withPosts: Boolean! = false, $withEmail: Boolean! = true) {
user(id: $id) {
id
name
email @include(if: $withEmail)
posts @include(if: $withPosts) {
id
title
}
}
}
Using Variables in TypeScript
import { gql, useQuery, TypedDocumentNode } from '@apollo/client';
// Type-safe query with variables
interface GetPostsData {
posts: Array<{
id: string;
title: string;
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
}>;
}
interface GetPostsVars {
limit: number;
offset?: number;
status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
search?: string;
}
const GET_POSTS: TypedDocumentNode<GetPostsData, GetPostsVars> = gql`
query GetPosts($limit: Int!, $offset: Int, $status: PostStatus, $search: String) {
posts(limit: $limit, offset: $offset, filter: { status: $status, search: $search }) {
id
title
status
}
}
`;
function PostList() {
const [status, setStatus] = useState<'DRAFT' | 'PUBLISHED' | undefined>();
const [search, setSearch] = useState('');
// Variables are fully typed
const { data, loading, fetchMore } = useQuery(GET_POSTS, {
variables: {
limit: 20,
offset: 0,
status,
search: search || undefined,
},
});
const loadMore = () => {
fetchMore({
variables: {
offset: data?.posts.length || 0,
},
});
};
return (
<div>
<input value={search} onChange={(e) => setSearch(e.target.value)} />
{data?.posts.map(post => <div key={post.id}>{post.title}</div>)}
<button onClick={loadMore}>Load More</button>
</div>
);
}
Key Takeaways
- Named fragments are reusable — define once, use in multiple operations
- Inline fragments handle polymorphism — required for unions and interfaces
- Co-locate fragments with components — each component declares its own data requirements
- Variables prevent injection — never concatenate user input into query strings
- Directives add conditionality — @include and @skip let clients control which fields are fetched