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

Fragment Consumption Patterns

Choosing between useFragment, useLazyLoadQuery, and other data hooks

Fragment Consumption Patterns

Master fragment consumption patterns with free flashcards and spaced repetition practice. This lesson covers container components, fragment composition, prop drilling solutions, and render prop patternsβ€”essential concepts for building scalable React applications with Relay.

Welcome to Fragment Consumption Patterns πŸ’»

When working with Relay, understanding how to consume fragments effectively is crucial for building maintainable, performant applications. Fragments define data requirements at the component level, but the real power comes from knowing how to structure your components to consume those fragments efficiently. This lesson explores the various patterns and best practices that will make your Relay code cleaner, more reusable, and easier to maintain.

Think of fragments like puzzle pieces πŸ§©β€”each component declares exactly what data it needs, and Relay assembles these pieces into efficient GraphQL queries. But just as important as creating the pieces is knowing how to fit them together in your component hierarchy.

Core Concepts: Understanding Fragment Consumption

🎯 The Container Component Pattern

The most fundamental pattern in Relay is the container componentβ€”a component that uses useFragment to consume fragment data and pass it to presentational components. This separation of concerns keeps your code organized and testable.

Structure of a Container Component:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Container Component             β”‚
β”‚  (Queries/Fragments + Logic)        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β€’ useFragment hook                 β”‚
β”‚  β€’ Data transformation              β”‚
β”‚  β€’ Event handlers                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ passes props
               ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Presentational Component          β”‚
β”‚   (Rendering only)                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  β€’ Pure rendering logic             β”‚
β”‚  β€’ No data fetching                 β”‚
β”‚  β€’ Easily testable                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Here's how this looks in practice:

import { useFragment, graphql } from 'react-relay';

// The fragment definition
const UserProfileFragment = graphql`
  fragment UserProfile_user on User {
    id
    name
    email
    avatarUrl
    memberSince
  }
`;

// Container component
function UserProfileContainer({ userRef }) {
  // Consume the fragment
  const user = useFragment(UserProfileFragment, userRef);
  
  // Transform data if needed
  const formattedDate = new Date(user.memberSince).toLocaleDateString();
  
  // Pass to presentational component
  return (
    <UserProfileView
      name={user.name}
      email={user.email}
      avatarUrl={user.avatarUrl}
      memberSince={formattedDate}
    />
  );
}

πŸ’‘ Pro Tip: The container handles the fragment reference (userRef), while the presentational component receives plain data. This makes testing much easierβ€”you can test the presentational component without any GraphQL setup!

πŸ”— Fragment Composition: Building Complex UIs

Fragment composition is the practice of combining multiple fragments to build complex component trees. Each component declares its own data requirements, and parent components compose these fragments together.

         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚   PageQuery      β”‚
         β”‚  (Root Query)    β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
      β”‚                       β”‚
      ↓                       ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Header  β”‚          β”‚   Content    β”‚
β”‚ Fragment β”‚          β”‚   Fragment   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                    β”‚                   β”‚
                    ↓                   ↓
              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
              β”‚  Post    β”‚        β”‚ Sidebar  β”‚
              β”‚ Fragment β”‚        β”‚ Fragment β”‚
              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Example of fragment composition:

// Child component declares its needs
const PostItemFragment = graphql`
  fragment PostItem_post on Post {
    id
    title
    excerpt
    publishedAt
  }
`;

function PostItem({ postRef }) {
  const post = useFragment(PostItemFragment, postRef);
  return <article>{post.title}</article>;
}

// Parent component includes child fragment
const PostListFragment = graphql`
  fragment PostList_query on Query {
    posts(first: 10) {
      edges {
        node {
          id
          ...PostItem_post
        }
      }
    }
  }
`;

function PostList({ queryRef }) {
  const data = useFragment(PostListFragment, queryRef);
  
  return (
    <div>
      {data.posts.edges.map(edge => (
        <PostItem key={edge.node.id} postRef={edge.node} />
      ))}
    </div>
  );
}

Key Benefits:

  • βœ… Locality: Each component owns its data requirements
  • βœ… Reusability: Components can be used anywhere
  • βœ… Type Safety: TypeScript/Flow can validate fragment usage
  • βœ… Performance: Relay optimizes the final query automatically
🎨 The Render Props Pattern

Sometimes you need more flexibility in how fragment data flows through your components. The render props pattern allows parent components to control rendering while child components manage data fetching.

const UserCardFragment = graphql`
  fragment UserCard_user on User {
    name
    role
    avatarUrl
  }
`;

function UserCard({ userRef, children }) {
  const user = useFragment(UserCardFragment, userRef);
  
  // Pass data to render prop
  return children(user);
}

// Usage with flexibility
function ProfilePage({ userRef }) {
  return (
    <UserCard userRef={userRef}>
      {(user) => (
        <div>
          <h1>{user.name}</h1>
          <span className={user.role === 'ADMIN' ? 'admin' : 'user'}>
            {user.role}
          </span>
        </div>
      )}
    </UserCard>
  );
}

πŸ’‘ When to Use Render Props:

  • When the parent needs to customize child rendering
  • For shared logic with different presentations
  • When building reusable data-fetching components
πŸ”„ Prop Drilling Solutions

Prop drilling occurs when you pass props through multiple component layers that don't need them, just to reach a deeply nested component. Relay's fragment pattern naturally reduces prop drilling!

❌ Without Fragments (Prop Drilling):

function Dashboard({ user }) {
  return <Sidebar user={user} />;
}

function Sidebar({ user }) {
  return <UserMenu user={user} />;
}

function UserMenu({ user }) {
  return <div>{user.name}</div>;
}

βœ… With Fragments (No Drilling):

// Dashboard only passes fragment reference
function Dashboard({ queryRef }) {
  const data = useFragment(DashboardFragment, queryRef);
  return <Sidebar sidebarRef={data} />;
}

// Sidebar doesn't need to know about user data
function Sidebar({ sidebarRef }) {
  const data = useFragment(SidebarFragment, sidebarRef);
  return <UserMenu userRef={data.currentUser} />;
}

// UserMenu declares exactly what it needs
function UserMenu({ userRef }) {
  const user = useFragment(UserMenuFragment, userRef);
  return <div>{user.name}</div>;
}
Aspect Prop Drilling Fragment Pattern
Data Coupling All layers know about data shape Only consuming component knows
Refactoring Change affects all layers Changes isolated to component
Testing Need full data object for tests Mock only fragment reference
Type Safety Manual prop type definitions Auto-generated from schema
🧩 Fragment Masking and Data Isolation

Fragment masking is Relay's secret weapon for preventing components from accessing data they didn't explicitly request. When you pass a fragment reference, child components can only access fields declared in their own fragments.

// Parent fragment
const ParentFragment = graphql`
  fragment Parent_user on User {
    id
    ...Child_user
  }
`;

// Child fragment
const ChildFragment = graphql`
  fragment Child_user on User {
    name
    email
  }
`;

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  
  // Parent can access: user.id
  // Parent CANNOT access: user.name, user.email (masked!)
  
  return <Child userRef={user} />;
}

function Child({ userRef }) {
  const user = useFragment(ChildFragment, userRef);
  
  // Child can access: user.name, user.email
  // Child CANNOT access: user.id (not in fragment)
  
  return <div>{user.name}</div>;
}

Why Fragment Masking Matters:

  • πŸ”’ Prevents Coupling: Components can't depend on ancestor data requirements
  • πŸ›‘οΈ Enforces Colocation: Data needs stay with the components that use them
  • πŸ”§ Safe Refactoring: Remove fields from parent without breaking children
  • πŸ“¦ Clear API: Fragment reference is the component's data contract

Examples: Real-World Fragment Consumption Patterns

Example 1: Social Feed with Nested Fragments

Let's build a social media feed where each post has comments, and each comment has author information. This demonstrates deep fragment composition:

// Deepest level: Comment author
const CommentAuthorFragment = graphql`
  fragment CommentAuthor_user on User {
    id
    name
    avatarUrl
  }
`;

function CommentAuthor({ userRef }) {
  const user = useFragment(CommentAuthorFragment, userRef);
  return (
    <div className="author">
      <img src={user.avatarUrl} alt={user.name} />
      <span>{user.name}</span>
    </div>
  );
}

// Middle level: Individual comment
const CommentFragment = graphql`
  fragment Comment_comment on Comment {
    id
    text
    createdAt
    author {
      ...CommentAuthor_user
    }
  }
`;

function Comment({ commentRef }) {
  const comment = useFragment(CommentFragment, commentRef);
  return (
    <div className="comment">
      <CommentAuthor userRef={comment.author} />
      <p>{comment.text}</p>
      <time>{comment.createdAt}</time>
    </div>
  );
}

// Top level: Post with comments
const PostFragment = graphql`
  fragment Post_post on Post {
    id
    title
    content
    comments(first: 5) {
      edges {
        node {
          id
          ...Comment_comment
        }
      }
    }
  }
`;

function Post({ postRef }) {
  const post = useFragment(PostFragment, postRef);
  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.content}</p>
      <div className="comments">
        {post.comments.edges.map(edge => (
          <Comment key={edge.node.id} commentRef={edge.node} />
        ))}
      </div>
    </article>
  );
}

What's Happening:

  1. Each component declares only what it needs
  2. Parent components include child fragments with ...FragmentName
  3. Relay automatically combines these into one efficient query
  4. Components remain loosely coupled and reusable
Example 2: Conditional Fragment Consumption

Sometimes you need to render different components based on data. Here's how to handle conditional fragment consumption:

const MediaItemFragment = graphql`
  fragment MediaItem_media on Media {
    __typename
    ... on Image {
      ...ImageViewer_image
    }
    ... on Video {
      ...VideoPlayer_video
    }
    ... on Document {
      ...DocumentPreview_document
    }
  }
`;

function MediaItem({ mediaRef }) {
  const media = useFragment(MediaItemFragment, mediaRef);
  
  // Render different components based on type
  switch (media.__typename) {
    case 'Image':
      return <ImageViewer imageRef={media} />;
    case 'Video':
      return <VideoPlayer videoRef={media} />;
    case 'Document':
      return <DocumentPreview documentRef={media} />;
    default:
      return <div>Unsupported media type</div>;
  }
}

πŸ’‘ Key Insight: Use __typename to determine which fragment was fulfilled, then pass the appropriate reference to specialized components.

Example 3: List Rendering with Fragment Arrays

When rendering lists, you'll often map over arrays of fragment references:

const TaskListFragment = graphql`
  fragment TaskList_project on Project {
    tasks(first: 20) {
      edges {
        node {
          id
          ...TaskItem_task
        }
      }
    }
  }
`;

const TaskItemFragment = graphql`
  fragment TaskItem_task on Task {
    id
    title
    status
    assignee {
      name
    }
  }
`;

function TaskList({ projectRef }) {
  const project = useFragment(TaskListFragment, projectRef);
  
  // Extract nodes for easier mapping
  const tasks = project.tasks.edges.map(edge => edge.node);
  
  // Group tasks by status
  const tasksByStatus = {
    TODO: tasks.filter(t => t.status === 'TODO'),
    IN_PROGRESS: tasks.filter(t => t.status === 'IN_PROGRESS'),
    DONE: tasks.filter(t => t.status === 'DONE')
  };
  
  return (
    <div className="task-board">
      {Object.entries(tasksByStatus).map(([status, statusTasks]) => (
        <div key={status} className="column">
          <h3>{status}</h3>
          {statusTasks.map(task => (
            <TaskItem key={task.id} taskRef={task} />
          ))}
        </div>
      ))}
    </div>
  );
}

function TaskItem({ taskRef }) {
  const task = useFragment(TaskItemFragment, taskRef);
  return (
    <div className="task-card">
      <h4>{task.title}</h4>
      <span>{task.assignee?.name || 'Unassigned'}</span>
    </div>
  );
}

Pattern Highlights:

  • Extract nodes from edges for cleaner code
  • Transform data (grouping, filtering) in the container
  • Pass individual fragment references to child components
Example 4: Higher-Order Component Pattern

For shared fragment consumption logic, you can create higher-order components (HOCs):

// HOC that adds loading/error states
function withFragmentContainer(Component, fragmentSpec, fragmentKey) {
  return function WrappedComponent(props) {
    const data = useFragment(fragmentSpec, props[fragmentKey]);
    
    // Could add loading states, error boundaries, etc.
    if (!data) {
      return <div>Loading...</div>;
    }
    
    return <Component {...props} data={data} />;
  };
}

// Usage
const UserProfileFragment = graphql`
  fragment UserProfileHOC_user on User {
    name
    email
    bio
  }
`;

function UserProfileView({ data }) {
  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.email}</p>
      <p>{data.bio}</p>
    </div>
  );
}

const UserProfile = withFragmentContainer(
  UserProfileView,
  UserProfileFragment,
  'userRef'
);

⚠️ Note: This pattern is less common in modern Relay (hooks are preferred), but it's useful when you need to share fragment consumption logic across many components.

Common Mistakes and How to Avoid Them ⚠️

Mistake 1: Forgetting Fragment Masking

❌ Wrong Approach:

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  
  // Trying to access masked field
  return <div>{user.email}</div>; // Error! email not in ParentFragment
}

βœ… Correct Approach:

const ParentFragment = graphql`
  fragment Parent_user on User {
    id
    email  # Add field to fragment
  }
`;

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  return <div>{user.email}</div>; // Works!
}

Fix: Always include fields in the fragment that declares the component consuming them.

Mistake 2: Passing Raw Data Instead of Fragment References

❌ Wrong Approach:

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  
  // Passing plain object instead of reference
  return <Child userData={{ name: user.name }} />;
}

function Child({ userData }) {
  // Can't use fragment on plain object!
  const user = useFragment(ChildFragment, userData); // Error!
}

βœ… Correct Approach:

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  
  // Pass the fragment reference
  return <Child userRef={user} />;
}

function Child({ userRef }) {
  const user = useFragment(ChildFragment, userRef); // Works!
}

Fix: Always pass fragment references (userRef), not extracted data objects.

Mistake 3: Not Spreading Child Fragments

❌ Wrong Approach:

const ParentFragment = graphql`
  fragment Parent_user on User {
    id
    name
    # Missing child fragment!
  }
`;

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  return <Child userRef={user} />; // Child fragment not fetched!
}

βœ… Correct Approach:

const ParentFragment = graphql`
  fragment Parent_user on User {
    id
    name
    ...Child_user  # Include child fragment
  }
`;

function Parent({ userRef }) {
  const user = useFragment(ParentFragment, userRef);
  return <Child userRef={user} />; // Works!
}

Fix: Use the spread operator (...) to include child fragments in parent queries.

Mistake 4: Over-Fetching in Fragments

❌ Wrong Approach:

const UserCardFragment = graphql`
  fragment UserCard_user on User {
    id
    name
    email
    address
    phoneNumber
    socialLinks
    preferences
    # ... fetching way more than needed
  }
`;

function UserCard({ userRef }) {
  const user = useFragment(UserCardFragment, userRef);
  // Only using name!
  return <div>{user.name}</div>;
}

βœ… Correct Approach:

const UserCardFragment = graphql`
  fragment UserCard_user on User {
    id
    name  # Only what's actually used
  }
`;

function UserCard({ userRef }) {
  const user = useFragment(UserCardFragment, userRef);
  return <div>{user.name}</div>;
}

Fix: Only request fields your component actually renders. Fragment colocation makes this naturalβ€”add fields as you use them.

Mistake 5: Incorrect Null Handling

❌ Wrong Approach:

function UserProfile({ userRef }) {
  const user = useFragment(UserProfileFragment, userRef);
  
  // Crashes if bio is null!
  return <div>{user.bio.substring(0, 100)}</div>;
}

βœ… Correct Approach:

function UserProfile({ userRef }) {
  const user = useFragment(UserProfileFragment, userRef);
  
  // Safely handle nullable fields
  return (
    <div>
      {user.bio ? user.bio.substring(0, 100) : 'No bio available'}
    </div>
  );
}

Fix: Check your GraphQL schema for nullable fields and handle them appropriately. Use optional chaining (?.) or explicit null checks.

Key Takeaways 🎯

πŸ“‹ Fragment Consumption Quick Reference

Pattern Use When Key Benefit
Container/Presentational Separating data from UI Testability
Fragment Composition Building component trees Reusability
Render Props Flexible rendering control Customization
Fragment Masking Enforcing data isolation Maintainability
Conditional Fragments Union/interface types Type safety

Essential Rules:

  1. βœ… Always spread child fragments: ...Child_fragment
  2. βœ… Pass fragment references, not plain objects
  3. βœ… Only request fields you actually use
  4. βœ… Handle nullable fields explicitly
  5. βœ… Keep fragments colocated with components
  6. βœ… Use __typename for conditional rendering
  7. βœ… Trust fragment maskingβ€”it prevents bugs!

🧠 Memory Device: The Fragment Flow

Remember fragment consumption with "CRISP":

  • Colocate: Keep fragments with components that use them
  • Reference: Pass fragment refs, not data objects
  • Include: Spread child fragments in parents
  • Spread: Use ...FragmentName syntax
  • Precise: Request only fields you render

πŸ€” Did You Know?

Relay's fragment masking was inspired by the principle of least privilege from computer security! Just as a program should only have access to resources it needs, a component should only access data it explicitly requests. This makes your codebase more secure against accidental coupling and easier to refactor.

Another interesting fact: Fragment composition can be arbitrarily deep. In large applications, it's not uncommon to see fragment chains 10+ levels deep, yet Relay compiles them into a single optimized query. The Relay compiler is smart enough to deduplicate fields and optimize the query structure automatically!

πŸ“š Further Study

  1. Relay Documentation - Fragments: https://relay.dev/docs/guided-tour/rendering/fragments/
  2. Thinking in Relay: https://relay.dev/docs/principles-and-architecture/thinking-in-relay/
  3. GraphQL Fragments Best Practices: https://www.apollographql.com/docs/react/data/fragments/

Next Steps: Now that you've mastered fragment consumption patterns, you're ready to explore pagination and list management with Relay's connection patterns. Practice building component hierarchies with nested fragments to solidify these concepts!