TechLead
Lesson 6 of 20
5 min read
GraphQL

Fragments and Variables

Use fragments for reusable query pieces and variables for dynamic, parameterized GraphQL operations

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

Continue Learning