You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering Relay

Fragments as Composition Units

Understanding why fragments are the atomic building blocks in Relay

Fragments as Composition Units

Master GraphQL Relay fragments with free flashcards and spaced repetition practice. This lesson covers fragments as reusable composition units, container component patterns, and fragment composition hierarchiesβ€”essential concepts for building scalable Relay applications.

Welcome to Fragment-Based Thinking 🧩

If you've been thinking of GraphQL queries as monolithic blocks of data requirements, it's time for a paradigm shift. In Relay, fragments are the fundamental building blocks of your data architecture. They're not just query syntaxβ€”they're composition units that mirror your component structure and enable true component-based data fetching.

Think of fragments like LEGO bricks: each piece is self-contained, reusable, and designed to snap together with other pieces. Just as you wouldn't build a LEGO castle in one giant, unmovable block, you shouldn't write queries as monolithic data requirements. Instead, you compose small, focused fragments that each component declares for itself.

Core Concepts: Why Fragments Are Composition Units πŸ’»

The Traditional Query Problem

In traditional GraphQL (without Relay), developers often write queries at the top level that fetch all data for an entire page:

query ProfilePage {
  user(id: "123") {
    name
    email
    avatar
    bio
    followerCount
    posts {
      id
      title
      createdAt
      likeCount
      comments {
        id
        text
        author {
          name
          avatar
        }
      }
    }
  }
}

This approach has critical problems:

  • Tight coupling: The parent component must know every data requirement of every child component
  • Maintenance nightmare: Changing a nested component requires updating queries far away in the component tree
  • No reusability: You can't reuse components without copying their data requirements
  • Fragile refactoring: Moving components breaks data fetching

The Relay Solution: Fragments as Units

Relay solves this by making each component declare its own data requirements using fragments:

// UserAvatar.js - declares what IT needs
const UserAvatar = ({ user }) => (
  <img src={user.avatar} alt={user.name} />
);

export default createFragmentContainer(UserAvatar, {
  user: graphql`
    fragment UserAvatar_user on User {
      name
      avatar
    }
  `
});

// PostItem.js - declares what IT needs
const PostItem = ({ post }) => (
  <div>
    <h3>{post.title}</h3>
    <UserAvatar user={post.author} />
  </div>
);

export default createFragmentContainer(PostItem, {
  post: graphql`
    fragment PostItem_post on Post {
      title
      author {
        ...UserAvatar_user  # Compose the child's fragment!
      }
    }
  `
});

Key insight: PostItem doesn't need to know that UserAvatar requires name and avatar. It just spreads the child's fragment using ...UserAvatar_user. This is fragment composition.

The Fragment Container Pattern πŸ—οΈ

A fragment container is a component wrapped with its data requirements. The pattern looks like this:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Component Logic             β”‚
β”‚  (render, event handlers, etc.)     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       Fragment Declaration          β”‚
β”‚  graphql`fragment X_y on Type { }`  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚      createFragmentContainer        β”‚
β”‚  (connects component to fragment)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        ↓
  Gets data as props automatically

Relay guarantees that when your component renders, the data specified in its fragment will be available in props. No manual fetching, no loading states at the component levelβ€”the data is just there.

Fragment Naming Convention πŸ“

Relay enforces a strict naming convention:

<ComponentName>_<propName>

Examples:

  • UserProfile_user - UserProfile component, data passed as user prop
  • CommentList_comments - CommentList component, data passed as comments prop
  • PostItem_post - PostItem component, data passed as post prop

This convention is not optional. Relay uses it to:

  • Generate TypeScript/Flow types automatically
  • Validate fragment spreads at compile time
  • Enable static analysis and refactoring tools
  • Make code grep-able and navigable

πŸ’‘ Tip: Think of the underscore as "belongs to". UserProfile_user means "the user data that belongs to UserProfile".

Fragment Composition Hierarchy 🌳

Fragments compose into trees that mirror your component tree:

QUERY (Root)
  β”‚
  └─ ProfilePage_user
       β”‚
       β”œβ”€ UserHeader_user
       β”‚    β”‚
       β”‚    β”œβ”€ UserAvatar_user
       β”‚    └─ UserBio_user
       β”‚
       └─ PostList_posts
            β”‚
            └─ PostItem_post (for each post)
                 β”‚
                 β”œβ”€ PostContent_post
                 └─ CommentSection_post
                      β”‚
                      └─ CommentItem_comment (for each)

Each component only knows about its immediate children's fragments. ProfilePage doesn't know that UserAvatar needs an avatar fieldβ€”it just spreads ...UserHeader_user, which in turn spreads ...UserAvatar_user.

Examples: Fragment Composition in Practice πŸ”§

Example 1: Building a User Card Component

Let's build a reusable UserCard from smaller fragments:

// Step 1: Avatar component (leaf node)
const UserAvatar = ({ user }) => (
  <img 
    src={user.avatarUrl} 
    alt={user.name}
    className="rounded-full w-12 h-12"
  />
);

export default createFragmentContainer(UserAvatar, {
  user: graphql`
    fragment UserAvatar_user on User {
      name
      avatarUrl
    }
  `
});

// Step 2: Badge component (leaf node)
const UserBadge = ({ user }) => (
  user.isPremium ? <span className="badge">⭐ Premium</span> : null
);

export default createFragmentContainer(UserBadge, {
  user: graphql`
    fragment UserBadge_user on User {
      isPremium
    }
  `
});

// Step 3: Stats component (leaf node)
const UserStats = ({ user }) => (
  <div className="stats">
    <span>{user.followerCount} followers</span>
    <span>{user.postCount} posts</span>
  </div>
);

export default createFragmentContainer(UserStats, {
  user: graphql`
    fragment UserStats_user on User {
      followerCount
      postCount
    }
  `
});

// Step 4: Compose into UserCard (parent node)
const UserCard = ({ user }) => (
  <div className="user-card">
    <UserAvatar user={user} />
    <div>
      <h3>{user.name}</h3>
      <UserBadge user={user} />
      <p>{user.headline}</p>
      <UserStats user={user} />
    </div>
  </div>
);

export default createFragmentContainer(UserCard, {
  user: graphql`
    fragment UserCard_user on User {
      name
      headline
      ...UserAvatar_user    # Compose avatar fragment
      ...UserBadge_user     # Compose badge fragment
      ...UserStats_user     # Compose stats fragment
    }
  `
});

What's happening here?

  1. Each leaf component (UserAvatar, UserBadge, UserStats) declares only the fields it renders
  2. UserCard composes these fragments using the spread operator (...)
  3. UserCard also declares its own direct requirements (name, headline)
  4. The final query sent to the server includes all fields, but components never see each other's data requirements in code

Example 2: List Rendering with Fragments

Handling lists requires understanding how fragments flow through arrays:

// CommentItem.js - renders one comment
const CommentItem = ({ comment }) => (
  <div className="comment">
    <UserAvatar user={comment.author} />
    <div>
      <p>{comment.text}</p>
      <span>{comment.createdAt}</span>
      {comment.canEdit && <button>Edit</button>}
    </div>
  </div>
);

export default createFragmentContainer(CommentItem, {
  comment: graphql`
    fragment CommentItem_comment on Comment {
      text
      createdAt
      canEdit
      author {
        ...UserAvatar_user
      }
    }
  `
});

// CommentList.js - renders multiple comments
const CommentList = ({ post }) => (
  <div>
    <h4>{post.commentCount} Comments</h4>
    {post.comments.edges.map(({ node }) => (
      <CommentItem key={node.id} comment={node} />
    ))}
  </div>
);

export default createFragmentContainer(CommentList, {
  post: graphql`
    fragment CommentList_post on Post {
      commentCount
      comments(first: 10) {
        edges {
          node {
            id
            ...CommentItem_comment  # Fragment spread for each item
          }
        }
      }
    }
  `
});

Critical insight: The fragment spread ...CommentItem_comment happens inside the node object. This tells Relay: "For each comment node in the connection, fetch the data specified by CommentItem_comment."

Example 3: Conditional Fragment Composition

Sometimes you render different components based on data:

// PostContent.js - handles different post types
const PostContent = ({ post }) => {
  switch (post.__typename) {
    case 'TextPost':
      return <TextPostContent post={post} />;
    case 'ImagePost':
      return <ImagePostContent post={post} />;
    case 'VideoPost':
      return <VideoPostContent post={post} />;
    default:
      return null;
  }
};

export default createFragmentContainer(PostContent, {
  post: graphql`
    fragment PostContent_post on Post {
      __typename
      ... on TextPost {
        ...TextPostContent_post
      }
      ... on ImagePost {
        ...ImagePostContent_post
      }
      ... on VideoPost {
        ...VideoPostContent_post
      }
    }
  `
});

Here we use inline fragments (... on TypeName) to conditionally fetch data based on the concrete type. Relay is smart enough to only fetch the relevant fragment for each item's actual type.

Example 4: Fragment Arguments and Directives

Fragments can accept arguments to customize their behavior:

const UserProfile = ({ user }) => (
  <div>
    <h1>{user.name}</h1>
    <img src={user.profilePicture.uri} />
    <PostList posts={user.posts} />
  </div>
);

export default createFragmentContainer(UserProfile, {
  user: graphql`
    fragment UserProfile_user on User 
    @argumentDefinitions(
      pictureSize: { type: "Int", defaultValue: 200 }
      includeEmail: { type: "Boolean!", defaultValue: false }
      postCount: { type: "Int", defaultValue: 10 }
    ) {
      name
      email @include(if: $includeEmail)
      profilePicture(size: $pictureSize) {
        uri
      }
      posts(first: $postCount) {
        ...PostList_posts
      }
    }
  `
});

// Parent component passes arguments when spreading:
const ProfilePage = ({ query }) => (
  <UserProfile 
    user={query.user} 
    pictureSize={400}
    includeEmail={true}
    postCount={20}
  />
);

The @argumentDefinitions directive makes fragments configurable. Parent components can pass different values when spreading the fragment, making it even more reusable.

Common Mistakes ⚠️

Mistake 1: Fetching Data the Component Doesn't Use

❌ Wrong:

const UserName = ({ user }) => <h1>{user.name}</h1>;

export default createFragmentContainer(UserName, {
  user: graphql`
    fragment UserName_user on User {
      name
      email          # Not used in render!
      followerCount  # Not used in render!
      posts { id }   # Not used in render!
    }
  `
});

βœ… Right:

const UserName = ({ user }) => <h1>{user.name}</h1>;

export default createFragmentContainer(UserName, {
  user: graphql`
    fragment UserName_user on User {
      name  # Only what's rendered
    }
  `
});

Why it matters: Relay's power comes from precise data requirements. Over-fetching defeats the purpose and bloats your queries.

Mistake 2: Skipping the Fragment Container

❌ Wrong:

// Just a regular component, no fragment
const UserCard = ({ user }) => (
  <div>
    <img src={user.avatar} />
    <h3>{user.name}</h3>
  </div>
);

export default UserCard; // No container!

This component can't be composed properly because it doesn't declare its data requirements. Parent components can't know what to fetch.

βœ… Right: Always wrap with createFragmentContainer and declare a fragment.

Mistake 3: Incorrect Fragment Naming

❌ Wrong:

export default createFragmentContainer(UserProfile, {
  user: graphql`
    fragment UserData on User { ... }  # Missing component name!
  `
});

❌ Wrong:

export default createFragmentContainer(UserProfile, {
  data: graphql`
    fragment UserProfile_user on User { ... }  # Prop name mismatch!
  `  // Prop is 'data' but fragment says '_user'
});

βœ… Right:

export default createFragmentContainer(UserProfile, {
  user: graphql`
    fragment UserProfile_user on User { ... }
  `  // ComponentName_propName matches!
});

Mistake 4: Forgetting to Spread Child Fragments

❌ Wrong:

const PostItem = ({ post }) => (
  <div>
    <h3>{post.title}</h3>
    <UserAvatar user={post.author} /> {/* Uses child component */}
  </div>
);

export default createFragmentContainer(PostItem, {
  post: graphql`
    fragment PostItem_post on Post {
      title
      author {
        name      # Manually fetching child's data
        avatar    # This breaks composition!
      }
    }
  `
});

βœ… Right:

const PostItem = ({ post }) => (
  <div>
    <h3>{post.title}</h3>
    <UserAvatar user={post.author} />
  </div>
);

export default createFragmentContainer(PostItem, {
  post: graphql`
    fragment PostItem_post on Post {
      title
      author {
        ...UserAvatar_user  # Let child declare its needs!
      }
    }
  `
});

Mistake 5: Creating "God Fragments"

❌ Wrong:

// One fragment to rule them all
const fragment = graphql`
  fragment User_all on User {
    id
    name
    email
    avatar
    bio
    followerCount
    followingCount
    postCount
    # ... 50 more fields
  }
`;

// Used everywhere, even when only 2 fields are needed

This defeats the entire purpose of fragments. Each component should have its own focused fragment.

βœ… Right: Create multiple small fragments, one per component, and compose them.

Key Takeaways 🎯

  1. Fragments are composition units, not just syntax. They represent component data dependencies.

  2. Each component declares its own data requirements using fragments. No component should know about its children's or siblings' data needs.

  3. Fragment containers connect components to data. Use createFragmentContainer(Component, fragments) for every data-driven component.

  4. The naming convention is mandatory: ComponentName_propName. Relay enforces this for good reasons.

  5. Compose fragments using the spread operator (...FragmentName). This is how you build your data dependency tree.

  6. Keep fragments minimal. Only fetch fields that the component actually renders. Over-fetching breaks Relay's optimization model.

  7. Fragments mirror your component tree. If you refactor components, the fragments automatically stay consistent.

  8. Use inline fragments (... on TypeName) for polymorphic types and type-specific data.

  9. Make fragments reusable with @argumentDefinitions when you need configurable behavior.

  10. Trust the compiler. Relay's compiler validates fragment composition at build time, catching errors before runtime.

πŸ“‹ Quick Reference: Fragment Composition Checklist

βœ“ Each component has a fragment containerUse createFragmentContainer
βœ“ Fragment name follows conventionComponentName_propName
βœ“ Fragment only includes used fieldsNo over-fetching
βœ“ Child fragments are spread, not duplicatedUse ...ChildFragment_prop
βœ“ Fragment matches prop namefragment X_user β†’ prop user
βœ“ Lists spread fragments in nodeedges { node { ...Frag } }

πŸ”§ Try This: Refactoring Exercise

Take an existing component in your project that fetches data at the top level. Try refactoring it:

  1. Identify all leaf components (components that don't render other data components)
  2. Create fragment containers for each leaf, declaring only their direct needs
  3. Work up the tree, creating fragments that spread child fragments
  4. Replace the top-level query with composed fragments

You'll notice your components become more portable, testable, and maintainable!

πŸ“š Further Study


Remember: Fragments aren't just a feature of GraphQLβ€”they're a design pattern for building scalable component architectures. When you think in fragments, you're thinking in composition, reusability, and maintainability. 🧩