Fragment Colocation Principle
Learn why components declare their own data requirements alongside their implementation
Fragment Colocation Principle
Master React Relay's fragment colocation with free flashcards and spaced repetition practice. This lesson covers data dependency colocating with components, fragment composition patterns, and type-safe data fetchingβessential concepts for building scalable GraphQL applications with Relay.
Welcome to Fragment Colocation π»
In traditional React applications, data requirements are often scattered across multiple filesβqueries defined in one location, components consuming data in another, and business logic somewhere else entirely. This separation creates maintenance nightmares: when you need to add a field to your UI, you hunt through query files, worry about breaking other components, and hope you've updated everything correctly.
Relay's fragment colocation principle solves this by keeping data requirements right next to the components that use them. Each component declares exactly what data it needs using fragments, and Relay automatically composes these fragments into efficient queries. This isn't just about convenienceβit's a fundamental shift in how we architect data-driven applications.
Think of it like a restaurant kitchen where each chef keeps their own ingredients and tools at their station (colocated) versus a chaotic kitchen where everyone shares one pantry and constantly gets in each other's way. Colocation creates clear boundaries, reduces conflicts, and makes the system easier to understand and modify.
Core Concepts: Understanding Colocation π§©
What is Fragment Colocation?
Fragment colocation means defining your data requirements in the same file as the component that consumes that data. Instead of writing queries in a separate "queries" folder, each component declares its data needs using GraphQL fragments.
Here's the anatomy of a colocated fragment:
βββββββββββββββββββββββββββββββββββββββββββββββ
β COMPONENT FILE β
βββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β import { graphql, useFragment } from... β
β β
β function UserProfile({ userRef }) { β
β const data = useFragment( β
β USER_FRAGMENT, β
β userRef β
β ); β
β β
β return {data.name}; β
β } β
β β
β const USER_FRAGMENT = graphql` β
β fragment UserProfile_user on User { β
β name β
β email β
β avatar β
β } β
β `; β
β β
βββββββββββββββββββββββββββββββββββββββββββββββ
β β
β β
Component UI Fragment Definition
(data consumer) (data requirements)
β
COLOCATED in same file!
Why Colocation Matters π―
1. Local Reasoning
When you open a component file, you immediately see:
- What data it needs (the fragment)
- How it renders that data (the JSX)
- What types are involved (TypeScript types generated from the fragment)
No jumping between files, no guessing about dependencies.
2. Safe Refactoring
Want to add a new field? Just:
- Add it to the fragment
- Use it in the component
- Done.
Relay's compiler ensures the field exists and generates proper TypeScript types. No other components are affected because they have their own fragments.
3. Automatic Composition
Relay composes child fragments into parent queries automatically:
ββββββββββββββββββββββββββββββββββββββββββββββββ
β PARENT COMPONENT β
β ββββββββββββββββββββββββββββββββββ β
β β Query: FeedQuery β β
β β user { β β
β β ...UserHeader_user β βββββ β
β β posts { β β β
β β ...PostList_posts β βββ β β
β β } β β β β
β β } β β β β
β ββββββββββββββββββββββββββββββββββ β β β
βββββββββββββββββββββββββββββββββββββββββΌββΌββββ
β β
βββββββββββββββββββββββββββββ β
β β
βββββββββββββ΄ββββββββββββββ ββββββββββββ΄ββββββββββ
β CHILD COMPONENT β β CHILD COMPONENT β
β βββββββββββββββββββββ β β ββββββββββββββββ β
β β Fragment: β β β β Fragment: β β
β β UserHeader_user β β β β PostList_ β β
β β name β β β β posts β β
β β avatar β β β β title β β
β βββββββββββββββββββββ β β β content β β
βββββββββββββββββββββββββββ β ββββββββββββββββ β
ββββββββββββββββββββββ
Parent spreads (...) child fragments
β Relay combines them into one query
β Each component gets exactly its data
The Fragment Spreading Pattern π€
Parent components "spread" child fragments using the ...FragmentName syntax:
// Parent query spreads child fragments
const FEED_QUERY = graphql`
query FeedQuery {
viewer {
...UserHeader_user
posts {
edges {
node {
...PostCard_post
}
}
}
}
}
`;
This pattern creates a data dependency graph that mirrors your component hierarchy. The parent doesn't need to know what fields UserHeader needsβit just spreads the fragment and passes the reference down.
Fragment References: The Key to Type Safety π
When you spread a fragment, Relay creates an opaque fragment referenceβa special value that can only be read by the fragment that created it:
| Concept | What It Looks Like | Purpose |
|---|---|---|
| Fragment Spread | ...PostCard_post |
Include child fragment in query |
| Fragment Reference | postRef: PostCard_post$key |
Opaque pointer to fragment data |
| useFragment Hook | useFragment(FRAGMENT, ref) |
Read data from fragment reference |
The reference is opaque intentionallyβthe parent can't peek at the data, ensuring that only the component that defined the fragment can access it. This prevents accidental coupling and makes refactoring safe.
Deep Dive: The Colocation Workflow π
Step-by-Step: Building a Colocated Component
Let's build a UserBadge component that displays a user's name and follower count:
Step 1: Define the component structure
import { graphql, useFragment } from 'react-relay';
import type { UserBadge_user$key } from './__generated__/UserBadge_user.graphql';
interface UserBadgeProps {
userRef: UserBadge_user$key; // Fragment reference type
}
function UserBadge({ userRef }: UserBadgeProps) {
// We'll read data here
return <div>Badge content</div>;
}
export default UserBadge;
Step 2: Define the fragment
const USER_BADGE_FRAGMENT = graphql`
fragment UserBadge_user on User {
name
followerCount
isVerified
}
`;
Step 3: Read the data
function UserBadge({ userRef }: UserBadgeProps) {
const data = useFragment(USER_BADGE_FRAGMENT, userRef);
return (
<div className="badge">
<span>{data.name}</span>
{data.isVerified && <VerifiedIcon />}
<span>{data.followerCount} followers</span>
</div>
);
}
Step 4: Use it in a parent
// Parent component
function UserProfile() {
const data = useLazyLoadQuery(
graphql`
query UserProfileQuery($userId: ID!) {
user(id: $userId) {
...UserBadge_user # Spread the fragment
bio
}
}
`,
{ userId: '123' }
);
return (
<div>
<UserBadge userRef={data.user} /> {/* Pass reference */}
<p>{data.bio}</p>
</div>
);
}
Fragment Naming Conventions π
Relay enforces a naming convention: ComponentName_propName
| Fragment Name | Component | Prop Name | Generated Type |
|---|---|---|---|
UserBadge_user |
UserBadge | user | UserBadge_user$key |
PostCard_post |
PostCard | post | PostCard_post$key |
CommentList_comments |
CommentList | comments | CommentList_comments$key |
This convention makes it easy to:
- Identify which component owns a fragment
- Know what prop should receive the data
- Find fragments in your codebase (search for
ComponentName_)
Multiple Fragments in One Component π
A component can use multiple fragments:
function PostDetail({ postRef, authorRef }) {
const post = useFragment(
graphql`
fragment PostDetail_post on Post {
title
content
publishedAt
}
`,
postRef
);
const author = useFragment(
graphql`
fragment PostDetail_author on User {
name
avatar
}
`,
authorRef
);
return (
<article>
<h1>{post.title}</h1>
<AuthorByline author={author} />
<div>{post.content}</div>
</article>
);
}
Each fragment follows the naming convention and gets its own reference.
Examples: Colocation in Action π
Example 1: Simple Profile Card
Let's build a profile card with proper colocation:
// ProfileCard.tsx
import { graphql, useFragment } from 'react-relay';
import type { ProfileCard_user$key } from './__generated__/ProfileCard_user.graphql';
interface Props {
userRef: ProfileCard_user$key;
}
function ProfileCard({ userRef }: Props) {
const user = useFragment(
graphql`
fragment ProfileCard_user on User {
id
name
email
profilePicture {
url
}
memberSince
}
`,
userRef
);
const memberDate = new Date(user.memberSince);
return (
<div className="profile-card">
<img src={user.profilePicture.url} alt={user.name} />
<h2>{user.name}</h2>
<p>{user.email}</p>
<p>Member since {memberDate.getFullYear()}</p>
</div>
);
}
export default ProfileCard;
What's happening here:
- TypeScript import: The generated type
ProfileCard_user$keyensures type safety - Fragment definition: Declares exactly the fields needed for this UI
- useFragment hook: Reads data from the reference
- Business logic: Date formatting happens in the component that needs it
- Self-contained: Everything about rendering this card is in one file
Parent usage:
// Dashboard.tsx
function Dashboard() {
const data = useLazyLoadQuery(
graphql`
query DashboardQuery {
viewer {
...ProfileCard_user # Just spread it!
}
}
`,
{}
);
return <ProfileCard userRef={data.viewer} />;
}
The parent doesn't know (or care) what fields ProfileCard needs. It just spreads the fragment and passes the reference.
Example 2: Nested Composition
Let's build a blog post with nested components:
// PostHeader.tsx
function PostHeader({ postRef }) {
const post = useFragment(
graphql`
fragment PostHeader_post on Post {
title
publishedAt
readTime
}
`,
postRef
);
return (
<header>
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<span>{post.readTime} min read</span>
</header>
);
}
// PostBody.tsx
function PostBody({ postRef }) {
const post = useFragment(
graphql`
fragment PostBody_post on Post {
content
images {
url
caption
}
}
`,
postRef
);
return (
<article>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
{post.images.map(img => (
<figure key={img.url}>
<img src={img.url} alt={img.caption} />
<figcaption>{img.caption}</figcaption>
</figure>
))}
</article>
);
}
// PostDetail.tsx (parent)
function PostDetail({ postRef }) {
const post = useFragment(
graphql`
fragment PostDetail_post on Post {
...PostHeader_post
...PostBody_post
author {
name
}
}
`,
postRef
);
return (
<div>
<PostHeader postRef={post} />
<PostBody postRef={post} />
<footer>By {post.author.name}</footer>
</div>
);
}
Composition flow:
βββββββββββββββββββββββββββββββββββββββ
β PostDetail Fragment β
β βββββββββββββββββββββββββββββββββ β
β β ...PostHeader_post β β βββ PostHeader
β β title β β gets only:
β β publishedAt β β title, publishedAt
β β readTime β β readTime
β β β β
β β ...PostBody_post β β βββ PostBody
β β content β β gets only:
β β images { url, caption } β β content, images
β β β β
β β author { name } β β βββ PostDetail
β βββββββββββββββββββββββββββββββββ β keeps:
βββββββββββββββββββββββββββββββββββββββ author.name
Each component sees ONLY its fragment data!
Notice that PostDetail spreads both child fragments AND has its own fields (author.name). This is perfectly validβparent components can mix spreads with their own data needs.
Example 3: Lists and Edges
Handling lists requires spreading fragments over arrays:
// CommentItem.tsx
function CommentItem({ commentRef }) {
const comment = useFragment(
graphql`
fragment CommentItem_comment on Comment {
id
text
author {
name
avatar
}
createdAt
}
`,
commentRef
);
return (
<div className="comment">
<img src={comment.author.avatar} alt="" />
<div>
<strong>{comment.author.name}</strong>
<p>{comment.text}</p>
<time>{comment.createdAt}</time>
</div>
</div>
);
}
// CommentList.tsx
function CommentList({ commentsRef }) {
const data = useFragment(
graphql`
fragment CommentList_comments on Post {
comments(first: 10) {
edges {
node {
id
...CommentItem_comment
}
}
}
}
`,
commentsRef
);
return (
<div className="comments">
<h3>Comments</h3>
{data.comments.edges.map(edge => (
<CommentItem
key={edge.node.id}
commentRef={edge.node}
/>
))}
</div>
);
}
Key pattern for lists:
- Parent fragment fetches the connection (edges/nodes)
- Parent spreads child fragment on
node - Parent maps over edges and passes each node reference to child
- Child reads its fragment data from the reference
This pattern works for any Relay connection (pagination-compatible lists).
Example 4: Conditional Fragments
Fragments can include conditional logic:
function UserProfile({ userRef, showPrivateInfo }) {
const user = useFragment(
graphql`
fragment UserProfile_user on User {
id
name
avatar
# Conditional field with directive
email @include(if: $showPrivateInfo)
phone @include(if: $showPrivateInfo)
# Inline fragments for different types
... on PremiumUser {
subscriptionTier
memberSince
}
... on FreeUser {
upgradePromptDismissed
}
}
`,
userRef
);
return (
<div>
<h1>{user.name}</h1>
<img src={user.avatar} alt="" />
{user.email && <p>Email: {user.email}</p>}
{user.subscriptionTier && (
<Badge tier={user.subscriptionTier} />
)}
{user.upgradePromptDismissed === false && (
<UpgradePrompt />
)}
</div>
);
}
Conditional patterns:
- @include(if: $variable): Include field only if variable is true
- @skip(if: $variable): Skip field if variable is true
- Inline fragments (
... on Type): Include fields for specific types - Check for undefined: In component, check if conditional fields exist
Common Mistakes β οΈ
Mistake 1: Accessing Data Without useFragment
β Wrong:
function UserBadge({ userRef }) {
// Trying to access userRef directly
return <div>{userRef.name}</div>; // ERROR!
}
β Correct:
function UserBadge({ userRef }) {
// Always use useFragment to read data
const user = useFragment(USER_BADGE_FRAGMENT, userRef);
return <div>{user.name}</div>;
}
Why: Fragment references are opaqueβyou can't access their properties directly. This enforces the colocation contract.
Mistake 2: Wrong Fragment Naming
β Wrong:
// Fragment name doesn't match component
const USER_FRAGMENT = graphql`
fragment userInfo on User { # Should be UserBadge_user
name
}
`;
β Correct:
const USER_FRAGMENT = graphql`
fragment UserBadge_user on User {
name
}
`;
Why: Relay requires the ComponentName_propName convention for code generation and type safety.
Mistake 3: Forgetting to Spread Fragment in Parent
β Wrong:
// Parent query
const QUERY = graphql`
query MyQuery {
user {
name # Fetching fields directly
email
}
}
`;
// Then passing to child
<UserBadge userRef={data.user} /> // ERROR: No fragment reference!
β Correct:
const QUERY = graphql`
query MyQuery {
user {
...UserBadge_user # Spread the fragment
}
}
`;
<UserBadge userRef={data.user} /> // Now data.user is a proper reference
Why: Fragment references are only created when you spread a fragment. Fetching fields directly doesn't create the reference.
Mistake 4: Over-fetching in Parent
β Wrong:
// Parent fetches everything "just in case"
const QUERY = graphql`
query MyQuery {
user {
id
name
email
phone
address
preferences
...UserBadge_user
}
}
`;
β Correct:
// Parent fetches only what it needs directly
const QUERY = graphql`
query MyQuery {
user {
id # Only if parent uses it
...UserBadge_user # Child declares its needs
}
}
`;
Why: Colocation means each component declares its own needs. The parent shouldn't fetch fields it doesn't useβlet children specify their requirements.
Mistake 5: Treating References as Regular Objects
β Wrong:
function Parent() {
const data = useLazyLoadQuery(QUERY, {});
// Trying to transform before passing
const transformedUser = {
...data.user,
displayName: data.user.name.toUpperCase()
};
return <UserBadge userRef={transformedUser} />; // ERROR!
}
β Correct:
function Parent() {
const data = useLazyLoadQuery(QUERY, {});
// Pass reference as-is
return <UserBadge userRef={data.user} />;
}
// If child needs transformation, do it after useFragment
function UserBadge({ userRef }) {
const user = useFragment(FRAGMENT, userRef);
const displayName = user.name.toUpperCase();
return <div>{displayName}</div>;
}
Why: Fragment references are opaque tokens. Don't spread them, transform them, or treat them as regular objects. Pass them directly to useFragment.
Mistake 6: Circular Fragment Dependencies
β Wrong:
// ComponentA spreads ComponentB's fragment
const FRAGMENT_A = graphql`
fragment ComponentA_data on Node {
...ComponentB_data
}
`;
// ComponentB spreads ComponentA's fragment
const FRAGMENT_B = graphql`
fragment ComponentB_data on Node {
...ComponentA_data # CIRCULAR!
}
`;
β Correct:
// Create a shared fragment for common data
const SHARED_FRAGMENT = graphql`
fragment SharedData_node on Node {
id
commonField
}
`;
const FRAGMENT_A = graphql`
fragment ComponentA_data on Node {
...SharedData_node
specificToA
}
`;
const FRAGMENT_B = graphql`
fragment ComponentB_data on Node {
...SharedData_node
specificToB
}
`;
Why: Circular dependencies break Relay's compiler. Extract shared needs into a separate fragment that both can spread.
Key Takeaways π―
π‘ The Colocation Contract
- Components declare their data needs using fragments
- Fragments live in the same file as the component
- Parents spread child fragments without knowing the details
- Relay composes everything into efficient queries
π‘ Benefits of Colocation
- Local reasoning: Everything you need is in one place
- Safe refactoring: Changes are localized to one component
- Type safety: Generated types catch errors at compile time
- Automatic optimization: Relay deduplicates and batches requests
π‘ The Fragment Reference Pattern
- Spreading a fragment creates an opaque reference
- References are typed with
FragmentName$key - Only
useFragmentcan read the reference - This enforces boundaries between components
π‘ Composition is Key
- Fragments compose like React components
- Use
...FragmentNameto spread child fragments - Parent queries stitch everything together
- Each component remains independent
π‘ Naming Matters
- Follow
ComponentName_propNameconvention strictly - Makes codebases searchable and predictable
- Enables proper type generation
- Signals ownership clearly
π Quick Reference: Fragment Colocation Checklist
| β Do | β Don't |
| Define fragments in same file as component | Put fragments in separate query files |
Use ComponentName_propName naming |
Use arbitrary fragment names |
Spread child fragments with ... |
Manually duplicate child fields |
| Pass fragment references as-is | Transform or spread references |
Use useFragment to read data |
Access reference properties directly |
| Let each component declare its needs | Over-fetch in parent "just in case" |
π§ Mental Model: The Component Tree = Data Tree
Your React Tree Your Fragment Tree<App> AppQuery { | viewer { <Feed> ...Feed_viewer | posts { <Post> ...Post_post | comments { <Comment> ...Comment_commentOne-to-one correspondence! Data flows down just like props.
π Further Study
Official Relay Documentation:
- Relay Fragments Guide - Official guide to fragments and colocation
- Thinking in Relay - Core principles behind Relay's design
- Relay Compiler - How Relay generates types and optimizes queries
π Next Steps:
You've mastered the fragment colocation principleβthe foundation of Relay's architecture. You now understand:
- Why colocation matters for maintainability
- How fragments compose to form queries
- The role of fragment references in type safety
- Common pitfalls and how to avoid them
Next in your journey: Learn about Fragment Containers and Composition Patterns to handle more complex scenarios like pagination, refetching, and optimistic updates.
Practice building components with colocated fragments, and notice how much easier it becomes to reason about your data layer. The initial learning curve pays off with dramatically improved developer experience! π