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

Composing Fragment Trees

Master spreading child fragments into parent queries to build complete data requirements

Composing Fragment Trees

Master fragment composition in Relay with free flashcards and hands-on examples. This lesson covers hierarchical data structures, colocation principles, and component composition patternsβ€”essential concepts for building maintainable GraphQL applications with Relay.

Welcome to Fragment Composition 🎯

When you first start with Relay, you might think about fetching all your data in one giant query at the top of your app. But fragment composition is where Relay's true power emerges. Think of fragments like LEGO blocksβ€”each component declares exactly what data it needs, and these declarations snap together naturally to form a complete data-fetching tree.

In traditional REST architectures, parent components often fetch data and pass it down through props. This creates tight coupling and makes refactoring painful. With fragment trees, each component is self-contained, declaring its own data requirements. When you compose components in your React tree, Relay automatically composes their fragments in the GraphQL query.

πŸ’‘ Key Insight: Fragment composition mirrors your component composition. If Component A renders Component B, Fragment A should spread Fragment B's fragment.

Core Concepts: Building Fragment Hierarchies 🌲

Let's explore the fundamental principles that make fragment composition work seamlessly in Relay applications.

1. The Composition Principle

Fragment composition follows a simple rule: spread child fragments where you render child components. This creates a natural alignment between your UI hierarchy and your data requirements.

Consider this React component hierarchy:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   UserProfile           β”‚
β”‚   (parent)              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚  UserHeader     β”‚    β”‚
β”‚  β”‚  (child)        β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚  UserPosts      β”‚    β”‚
β”‚  β”‚  (child)        β”‚    β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The corresponding fragment structure mirrors this exactly:

// UserProfile.js
const UserProfileFragment = graphql`
  fragment UserProfileFragment on User {
    id
    # Spread child fragments
    ...UserHeaderFragment
    ...UserPostsFragment
  }
`;

function UserProfile({ user }) {
  return (
    <div>
      <UserHeader user={user} />
      <UserPosts user={user} />
    </div>
  );
}

2. Data Masking and Encapsulation πŸ”’

One of Relay's most powerful features is data masking. Even though UserProfile spreads UserHeaderFragment, it cannot access the data that UserHeaderFragment requests. Only the UserHeader component can read its own fragment data.

Why is this important?

  • Prevents over-fetching: Components can't accidentally depend on data they don't declare
  • Enables safe refactoring: Change a child's data requirements without breaking parents
  • Enforces boundaries: Each component owns its data contract
// UserProfile CANNOT do this:
function UserProfile({ user }) {
  console.log(user.avatarUrl); // ❌ Undefined! This field is in UserHeaderFragment
  return <UserHeader user={user} />;
}

// Only UserHeader can access it:
function UserHeader({ user }) {
  const data = useFragment(UserHeaderFragment, user);
  console.log(data.avatarUrl); // βœ… Works! This component declared the field
}

3. Fragment Spreading Syntax πŸ“

Relay uses the spread operator (...) to compose fragments. This tells Relay: "Include all the fields from this other fragment here."

Three spread patterns you'll use constantly:

Pattern Syntax Use Case
Spread child fragment ...ChildFragment Most common - parent includes child's data
Spread on field posts { ...PostFragment } Fragment applies to a nested object or list
Multiple spreads ...Fragment1
...Fragment2
Component renders multiple children

4. The Colocation Pattern πŸ“

Colocation means keeping your fragment definition in the same file as the component that uses it. This is a Relay best practice that makes your code maintainable and self-documenting.

// βœ… GOOD: Fragment colocated with component
// UserCard.js
import { graphql, useFragment } from 'react-relay';

const UserCardFragment = graphql`
  fragment UserCardFragment on User {
    name
    email
    memberSince
  }
`;

export default function UserCard({ userRef }) {
  const user = useFragment(UserCardFragment, userRef);
  return <div>{user.name}</div>;
}
// ❌ BAD: Fragment defined far from component
// fragments.js (separate file)
export const UserCardFragment = graphql`...`;

// UserCard.js (different file)
import { UserCardFragment } from './fragments';
// Now readers must jump between files to understand data requirements

🧠 Memory Device: "Fragment lives where component thrives" - keep them together!

Practical Examples: Real-World Fragment Trees πŸ”§

Let's build progressively complex fragment compositions to solidify your understanding.

Example 1: Simple Parent-Child Composition

Imagine a social media feed. Each post has an author, and we want to display the author's profile picture and name.

// AuthorBadge.js - Child component
import { graphql, useFragment } from 'react-relay';

const AuthorBadgeFragment = graphql`
  fragment AuthorBadgeFragment on User {
    name
    avatarUrl
    isVerified
  }
`;

function AuthorBadge({ userRef }) {
  const user = useFragment(AuthorBadgeFragment, userRef);
  
  return (
    <div className="author-badge">
      <img src={user.avatarUrl} alt={user.name} />
      <span>{user.name}</span>
      {user.isVerified && <VerifiedIcon />}
    </div>
  );
}

export default AuthorBadge;
// PostItem.js - Parent component
import { graphql, useFragment } from 'react-relay';
import AuthorBadge from './AuthorBadge';

const PostItemFragment = graphql`
  fragment PostItemFragment on Post {
    id
    content
    createdAt
    author {
      # Spread the child's fragment on the author field
      ...AuthorBadgeFragment
    }
  }
`;

function PostItem({ postRef }) {
  const post = useFragment(PostItemFragment, postRef);
  
  return (
    <article>
      {/* Pass the author ref to child */}
      <AuthorBadge userRef={post.author} />
      <p>{post.content}</p>
      <time>{post.createdAt}</time>
    </article>
  );
}

export default PostItem;

What's happening here:

  1. AuthorBadge declares it needs name, avatarUrl, and isVerified from a User
  2. PostItem has an author field that returns a User type
  3. PostItem spreads ...AuthorBadgeFragment on the author field
  4. When Relay compiles this, it automatically includes all of AuthorBadge's fields in the query
  5. At runtime, PostItem passes the opaque author reference to AuthorBadge
  6. AuthorBadge uses useFragment to "unwrap" only the data it declared

Example 2: Lists and Multiple Children

Now let's compose fragments for a list. We'll build a comment thread where each comment can have replies (nested comments).

// CommentItem.js
import { graphql, useFragment } from 'react-relay';
import AuthorBadge from './AuthorBadge';

const CommentItemFragment = graphql`
  fragment CommentItemFragment on Comment {
    id
    text
    likeCount
    author {
      ...AuthorBadgeFragment
    }
    # Self-referential: comments can have reply comments
    replies {
      ...CommentItemFragment
    }
  }
`;

function CommentItem({ commentRef }) {
  const comment = useFragment(CommentItemFragment, commentRef);
  
  return (
    <div className="comment">
      <AuthorBadge userRef={comment.author} />
      <p>{comment.text}</p>
      <span>{comment.likeCount} likes</span>
      
      {/* Recursive: render replies with same component */}
      {comment.replies && comment.replies.length > 0 && (
        <div className="replies">
          {comment.replies.map(reply => (
            <CommentItem key={reply.id} commentRef={reply} />
          ))}
        </div>
      )}
    </div>
  );
}

export default CommentItem;
// CommentThread.js
import { graphql, useFragment } from 'react-relay';
import CommentItem from './CommentItem';

const CommentThreadFragment = graphql`
  fragment CommentThreadFragment on Post {
    id
    comments {
      ...CommentItemFragment
    }
  }
`;

function CommentThread({ postRef }) {
  const post = useFragment(CommentThreadFragment, postRef);
  
  return (
    <section className="comment-thread">
      <h3>Comments</h3>
      {post.comments.map(comment => (
        <CommentItem key={comment.id} commentRef={comment} />
      ))}
    </section>
  );
}

export default CommentThread;

Fragment tree visualization:

CommentThreadFragment
    β”‚
    └─→ comments []
            β”‚
            └─→ CommentItemFragment (spread per item)
                    β”œβ”€β†’ author
                    β”‚      └─→ AuthorBadgeFragment
                    β”‚
                    └─→ replies []
                            └─→ CommentItemFragment (recursive!)

πŸ’‘ Pro Tip: Notice CommentItemFragment spreads itself on replies. This recursive fragment pattern is perfectly valid and enables nested tree structures!

Example 3: Sibling Components Sharing a Parent

Let's build a user profile page with multiple sectionsβ€”each section is a separate component with its own fragment.

// ProfileHeader.js
const ProfileHeaderFragment = graphql`
  fragment ProfileHeaderFragment on User {
    name
    bio
    coverPhotoUrl
    followerCount
    followingCount
  }
`;

function ProfileHeader({ userRef }) {
  const user = useFragment(ProfileHeaderFragment, userRef);
  return (
    <header style={{ backgroundImage: `url(${user.coverPhotoUrl})` }}>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
      <div>
        <span>{user.followerCount} followers</span>
        <span>{user.followingCount} following</span>
      </div>
    </header>
  );
}
// ProfileStats.js
const ProfileStatsFragment = graphql`
  fragment ProfileStatsFragment on User {
    postCount
    likeCount
    commentCount
    joinedDate
  }
`;

function ProfileStats({ userRef }) {
  const user = useFragment(ProfileStatsFragment, userRef);
  return (
    <aside>
      <h3>Activity</h3>
      <ul>
        <li>{user.postCount} posts</li>
        <li>{user.likeCount} likes given</li>
        <li>{user.commentCount} comments</li>
        <li>Joined {user.joinedDate}</li>
      </ul>
    </aside>
  );
}
// ProfilePosts.js
const ProfilePostsFragment = graphql`
  fragment ProfilePostsFragment on User {
    posts(first: 10) {
      edges {
        node {
          ...PostItemFragment
        }
      }
    }
  }
`;

function ProfilePosts({ userRef }) {
  const user = useFragment(ProfilePostsFragment, userRef);
  const posts = user.posts.edges.map(edge => edge.node);
  
  return (
    <section>
      <h3>Recent Posts</h3>
      {posts.map(post => (
        <PostItem key={post.id} postRef={post} />
      ))}
    </section>
  );
}
// UserProfilePage.js - The parent assembling everything
import { graphql, useFragment } from 'react-relay';
import ProfileHeader from './ProfileHeader';
import ProfileStats from './ProfileStats';
import ProfilePosts from './ProfilePosts';

const UserProfilePageFragment = graphql`
  fragment UserProfilePageFragment on User {
    id
    # Spread all sibling fragments
    ...ProfileHeaderFragment
    ...ProfileStatsFragment
    ...ProfilePostsFragment
  }
`;

function UserProfilePage({ userRef }) {
  const user = useFragment(UserProfilePageFragment, userRef);
  
  return (
    <div className="profile-page">
      <ProfileHeader userRef={user} />
      <div className="profile-content">
        <ProfileStats userRef={user} />
        <ProfilePosts userRef={user} />
      </div>
    </div>
  );
}

export default UserProfilePage;

The composition pattern:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   UserProfilePageFragment              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                        β”‚
β”‚   ...ProfileHeaderFragment  ───────┐   β”‚
β”‚   ...ProfileStatsFragment   ──────┼─┐ β”‚
β”‚   ...ProfilePostsFragment   ─────┼┼─┼──
β”‚                                  β”‚β”‚β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”Όβ”Όβ”€β”Όβ”€β”˜
                                   β”‚β”‚β”‚ β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚β”‚ β”‚
          β”‚           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚ β”‚
          β”‚           β”‚       β”Œβ”€β”€β”€β”€β”€β”€β”˜ β”‚
          β–Ό           β–Ό       β–Ό        β–Ό
    ProfileHeader ProfileStats ProfilePosts
        renders      renders      renders
        header       stats        post list

πŸ€” Did you know? All three sibling components receive the same userRef, but each extracts different fields via useFragment. This is data masking in actionβ€”they share a reference but not visibility!

Example 4: Deep Nesting with Conditional Rendering

Real applications often have conditional UI based on data. Let's see how fragments compose when components render conditionally.

// MediaAttachment.js - Shows image or video
const MediaAttachmentFragment = graphql`
  fragment MediaAttachmentFragment on Media {
    __typename
    ... on Image {
      imageUrl
      altText
      width
      height
    }
    ... on Video {
      videoUrl
      thumbnailUrl
      duration
    }
  }
`;

function MediaAttachment({ mediaRef }) {
  const media = useFragment(MediaAttachmentFragment, mediaRef);
  
  if (media.__typename === 'Image') {
    return (
      <img 
        src={media.imageUrl} 
        alt={media.altText}
        width={media.width}
        height={media.height}
      />
    );
  }
  
  if (media.__typename === 'Video') {
    return (
      <video poster={media.thumbnailUrl} controls>
        <source src={media.videoUrl} />
      </video>
    );
  }
  
  return null;
}
// RichPost.js - Post with optional media
const RichPostFragment = graphql`
  fragment RichPostFragment on Post {
    id
    content
    hasMedia
    media @include(if: $includeMedia) {
      ...MediaAttachmentFragment
    }
    author {
      ...AuthorBadgeFragment
    }
  }
`;

function RichPost({ postRef }) {
  const post = useFragment(RichPostFragment, postRef);
  
  return (
    <article>
      <AuthorBadge userRef={post.author} />
      <p>{post.content}</p>
      {post.hasMedia && post.media && (
        <MediaAttachment mediaRef={post.media} />
      )}
    </article>
  );
}

Key concepts demonstrated:

  • Union types: MediaAttachmentFragment handles multiple types with inline fragments (... on Image)
  • Directives: @include(if: $includeMedia) conditionally fetches the media field
  • Type safety: __typename lets us conditionally render based on actual type

Common Mistakes to Avoid ⚠️

Even experienced developers make these fragment composition errors. Learn to recognize and avoid them!

Mistake 1: Forgetting to Spread Child Fragments

// ❌ WRONG: Parent doesn't spread child fragment
const ParentFragment = graphql`
  fragment ParentFragment on User {
    id
    name
    # Missing: ...ChildFragment
  }
`;

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  // This will break! Child expects data that wasn't fetched
  return <Child userRef={user} />;
}

Fix: Always spread child fragments where you render child components:

// βœ… CORRECT
const ParentFragment = graphql`
  fragment ParentFragment on User {
    id
    name
    ...ChildFragment
  }
`;

Mistake 2: Spreading on Wrong Field Type

// ❌ WRONG: Fragment expects User, but spreading on Post
const PostPageFragment = graphql`
  fragment PostPageFragment on Post {
    id
    title
    # UserCardFragment expects a User, but Post.id is an ID, not a User!
    ...UserCardFragment
  }
`;

Fix: Spread fragments on fields that match the expected type:

// βœ… CORRECT
const PostPageFragment = graphql`
  fragment PostPageFragment on Post {
    id
    title
    author {  # author field returns a User
      ...UserCardFragment
    }
  }
`;

Mistake 3: Trying to Access Masked Data

// ❌ WRONG: Parent tries to read child's fragment data
function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  
  // This is undefined! avatarUrl is in ChildFragment, not ParentFragment
  console.log(user.avatarUrl);
  
  return <Child userRef={user} />;
}

Fix: Declare fields in the component that needs them:

// βœ… CORRECT: If parent needs the field, declare it
const ParentFragment = graphql`
  fragment ParentFragment on User {
    avatarUrl  # Now parent can access it
    ...ChildFragment
  }
`;

Mistake 4: Not Colocating Fragments

// ❌ WRONG: Fragment in separate file from component
// fragments/user.js
export const UserCardFragment = graphql`...`;

// components/UserCard.js
import { UserCardFragment } from '../fragments/user';
// Hard to maintain - readers must jump between files

Fix: Keep fragment with its component:

// βœ… CORRECT: Fragment colocated in UserCard.js
import { graphql, useFragment } from 'react-relay';

const UserCardFragment = graphql`
  fragment UserCardFragment on User { ... }
`;

function UserCard({ userRef }) {
  // Fragment and component together - easy to understand!
}

Mistake 5: Circular Fragment Dependencies

// ❌ WRONG: Creates infinite loop
// ComponentA.js
const FragmentA = graphql`
  fragment FragmentA on Node {
    ...FragmentB  # A depends on B
  }
`;

// ComponentB.js
const FragmentB = graphql`
  fragment FragmentB on Node {
    ...FragmentA  # B depends on A - circular!
  }
`;

Fix: Restructure your component hierarchy or extract shared fields:

// βœ… CORRECT: Shared fragment for common fields
const SharedFragment = graphql`
  fragment SharedFragment on Node {
    id
    commonField
  }
`;

const FragmentA = graphql`
  fragment FragmentA on Node {
    ...SharedFragment
    specificToA
  }
`;

const FragmentB = graphql`
  fragment FragmentB on Node {
    ...SharedFragment
    specificToB
  }
`;

Key Takeaways πŸŽ“

Let's consolidate everything you've learned about composing fragment trees:

Core Principles:

  1. Mirror your UI hierarchy - Fragment composition should match component composition
  2. Spread child fragments - Use ...ChildFragment where you render child components
  3. Respect data masking - Components can only read fields they declare
  4. Colocate fragments - Keep fragments in the same file as their components
  5. Think in trees - Your app has a component tree and a parallel fragment tree

Practical Rules:

  • βœ… DO spread fragments on correctly typed fields
  • βœ… DO use recursive fragments for tree structures (comments, folders, etc.)
  • βœ… DO declare fields in the component that uses them
  • βœ… DO keep fragments small and focused
  • ❌ DON'T try to access child fragment data from parent
  • ❌ DON'T create circular fragment dependencies
  • ❌ DON'T fetch data "just in case" - be precise

Mental Model:

      React Component Tree          GraphQL Fragment Tree
            β”‚                              β”‚
            β”‚                              β”‚
      β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”                  β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
      β”‚   App     β”‚                  β”‚ AppQuery  β”‚
      β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                  β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
            β”‚                              β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”               β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
    β”‚                β”‚               β”‚            β”‚
β”Œβ”€β”€β”€β”΄β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”΄β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
β”‚Header β”‚      β”‚ Content β”‚      β”‚HeaderF β”‚  β”‚ ContentF β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
                    β”‚                             β”‚
              β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”                 β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”
              β”‚           β”‚                 β”‚           β”‚
          β”Œβ”€β”€β”€β”΄β”€β”€β”    β”Œβ”€β”€β”€β”΄β”€β”€β”          β”Œβ”€β”€β”€β”΄β”€β”€β”    β”Œβ”€β”€β”€β”΄β”€β”€β”
          β”‚ Post β”‚    β”‚ Side β”‚          β”‚PostF β”‚    β”‚SideF β”‚
          β””β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”˜

        Composition matches!         Spreads mirror renders!

πŸ”§ Try this: Take an existing component in your app. Identify all its children. Check if its fragment spreads all child fragments. If not, add the missing spreads!

πŸ“‹ Quick Reference Card: Fragment Composition Patterns

Pattern Code Example When to Use
Basic Spread ...ChildFragment Parent renders one child component
Field Spread author { ...UserFragment } Fragment applies to nested object
List Spread posts { ...PostFragment } Fragment applies to each list item
Multiple Spreads ...Fragment1
...Fragment2
Parent renders multiple children
Recursive Spread replies { ...CommentFragment } Tree structures (comments, folders)
Conditional Spread media @include(if: $var) { ...MediaFragment } Optional data based on variables
Union Type Spread ... on Image { ...ImageFragment } Different types in same field

Hook Pattern:

const data = useFragment(MyFragment, dataRef);
  • MyFragment: The GraphQL fragment definition
  • dataRef: Opaque reference from parent (fragment key)
  • data: Typed object with only declared fields

Component Pattern:

// 1. Define fragment
const Fragment = graphql`fragment ComponentFragment on Type { fields }`;

// 2. Use in component
function Component({ dataRef }) {
  const data = useFragment(Fragment, dataRef);
  return <div>{data.fields}</div>;
}

// 3. Export for parent to spread
export default Component;

πŸ“š Further Study

Ready to dive deeper into Relay fragment composition? Explore these resources:


You've mastered fragment composition! πŸŽ‰ You now understand how to build maintainable, scalable Relay applications by composing fragments that mirror your component tree. Next, you'll learn about fragment variables and how to parameterize your data requirements.

Remember: Think in fragments, compose in trees, colocate for success! 🌲