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 |
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:
AuthorBadgedeclares it needsname,avatarUrl, andisVerifiedfrom aUserPostItemhas anauthorfield that returns aUsertypePostItemspreads...AuthorBadgeFragmenton theauthorfield- When Relay compiles this, it automatically includes all of
AuthorBadge's fields in the query - At runtime,
PostItempasses the opaqueauthorreference toAuthorBadge AuthorBadgeusesuseFragmentto "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:
MediaAttachmentFragmenthandles multiple types with inline fragments (... on Image) - Directives:
@include(if: $includeMedia)conditionally fetches the media field - Type safety:
__typenamelets 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:
- Mirror your UI hierarchy - Fragment composition should match component composition
- Spread child fragments - Use
...ChildFragmentwhere you render child components - Respect data masking - Components can only read fields they declare
- Colocate fragments - Keep fragments in the same file as their components
- 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 |
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 definitiondataRef: 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:
- Relay Documentation - Fragment Container: https://relay.dev/docs/api-reference/use-fragment/ - Official guide to the
useFragmenthook and data masking - Relay Examples Repository: https://github.com/relayjs/relay-examples - Real-world examples showing complex fragment trees in production apps
- GraphQL Fragments Best Practices: https://www.apollographql.com/docs/react/data/fragments/#fragment-best-practices - Complementary patterns from Apollo that apply to Relay
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! π²