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

Parent-Child Data Boundaries

Why parents cannot reach into child fragment data and what this prevents

Parent-Child Data Boundaries

Master parent-child data boundaries in Relay with free flashcards and spaced repetition practice. This lesson covers data isolation strategies, boundary enforcement patterns, and access control mechanismsβ€”essential concepts for building secure, maintainable GraphQL applications with Relay's fragment-based architecture.

Welcome to Data Masking Fundamentals

πŸ’» In Relay's fragment architecture, parent-child data boundaries define how data flows (or doesn't flow) between components in your React tree. Unlike traditional prop drilling or global state patterns, Relay enforces data maskingβ€”a parent component cannot access data declared in a child's fragment, and vice versa. This architectural decision prevents tight coupling and creates clear contracts between components.

Think of it like privacy walls between apartments in a building. Each tenant (component) has access only to their own space (declared fragments), even though they're all part of the same structure (component tree). The landlord (Relay) enforces these boundaries automatically.

Core Concepts

πŸ”’ What Are Data Boundaries?

A data boundary is the invisible barrier that prevents a component from accessing fragment data declared by its ancestors or descendants. When you declare a fragment in a component, that data is masked from all other componentsβ€”they see only an opaque reference, not the actual data.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ParentComponent                    β”‚
β”‚  fragment Parent on User {          β”‚
β”‚    id                               β”‚
β”‚    name  ← Parent can access        β”‚
β”‚  }                                  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  ChildComponent               β”‚ β”‚
β”‚  β”‚  fragment Child on User {     β”‚ β”‚
β”‚  β”‚    email    ← Child only     β”‚ β”‚
β”‚  β”‚    phone                      β”‚ β”‚
β”‚  β”‚  }                            β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Parent CANNOT access email/phone
Child CANNOT access name

🎯 Why Boundaries Matter

1. Prevents Coupling πŸ”—
Without boundaries, changing a child's data requirements could break parent components that depended on that data. With masking, each component's data contract is explicit and isolated.

2. Enables Refactoring πŸ”„
You can move components around the tree or change their internal data needs without cascading changes throughout your codebase.

3. Improves Performance ⚑
Relay can optimize re-renders because it knows exactly which components need updates when specific data changes.

4. Enforces Encapsulation πŸ“¦
Components become true black boxesβ€”their implementation details (including data needs) are hidden from the outside world.

πŸ›‘οΈ How Relay Enforces Boundaries

Relay uses opaque fragment references at runtime. When you spread a fragment, you receive a reference object that's meaningless without the corresponding useFragment hook:

// ParentComponent.jsx
import {graphql, useFragment} from 'react-relay';

function ParentComponent({queryRef}) {
  const data = useFragment(
    graphql`
      fragment ParentComponent_user on User {
        id
        name
        ...ChildComponent_user  # Spread child's fragment
      }
    `,
    queryRef
  );

  console.log(data.name);  // βœ… Works - declared in Parent
  console.log(data.email); // ❌ Undefined - declared in Child
  
  // data.ChildComponent_user is an opaque reference
  return (
    <div>
      <h1>{data.name}</h1>
      <ChildComponent user={data} />  {/* Pass the reference */}
    </div>
  );
}
// ChildComponent.jsx
function ChildComponent({user}) {
  const data = useFragment(
    graphql`
      fragment ChildComponent_user on User {
        email
        phone
      }
    `,
    user  // Unwrap the reference here
  );

  console.log(data.email); // βœ… Works - declared in Child
  console.log(data.name);  // ❌ Undefined - declared in Parent
  
  return <div>{data.email}</div>;
}

Key insight: The parent spreads ...ChildComponent_user but receives only an opaque reference. The child must call useFragment with that reference to "unlock" the actual data.

πŸ—οΈ Boundary Patterns in Practice

Pattern 1: Strict Parent-Child Isolation

The most common patternβ€”each component declares exactly the data it needs, nothing more:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ProfilePage                   β”‚
β”‚  fragment ProfilePage {        β”‚
β”‚    viewer { id }               β”‚
β”‚    ...ProfileHeader_viewer     β”‚
β”‚    ...ProfileContent_viewer    β”‚
β”‚  }                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ ProfileHeader            β”‚ β”‚
β”‚  β”‚ fragment { avatar, name }β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ ProfileContent           β”‚ β”‚
β”‚  β”‚ fragment { bio, posts }  β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Benefits:

  • Each component is independently testable
  • No shared data dependencies
  • Clear ownership boundaries
Pattern 2: Shared Data with Duplication

When multiple components need the same field, they each declare it independently:

// Parent needs user ID for analytics
fragment Parent_user on User {
  id  # Declared here
  name
  ...Child_user
}

// Child also needs user ID for routing
fragment Child_user on User {
  id  # Declared again - that's OK!
  email
}

πŸ’‘ Relay deduplicates these requests automaticallyβ€”the network fetches id only once, even though it appears in multiple fragments.

Pattern 3: Coordinated Boundaries with @arguments

Sometimes a parent needs to influence a child's data fetching without accessing the data itself:

// Parent controls pagination size
fragment Parent_user on User {
  id
  ...PostList_user @arguments(count: 10)
}

// Child receives the argument
fragment PostList_user on User 
  @argumentDefinitions(count: {type: "Int!", defaultValue: 5}) {
  posts(first: $count) {
    edges { node { id, title } }
  }
}

The parent sets count: 10 but never sees the posts dataβ€”the boundary remains intact.

⚠️ Boundary Violations and Anti-Patterns

Anti-Pattern 1: Prop Drilling Fragment Data

❌ Wrong approach:

function Parent({queryRef}) {
  const data = useFragment(parentFragment, queryRef);
  const childData = useFragment(childFragment, data); // WRONG!
  return <Child data={childData} />;  // Breaking encapsulation
}

βœ… Correct approach:

function Parent({queryRef}) {
  const data = useFragment(parentFragment, queryRef);
  return <Child userRef={data} />;  // Pass opaque reference
}

function Child({userRef}) {
  const data = useFragment(childFragment, userRef);  // Child unwraps
  return <div>{data.email}</div>;
}
Anti-Pattern 2: Accessing Masked Fields

❌ Wrong:

fragment Parent_user on User {
  ...Child_user
}

// Later in component
const {email} = data;  // Undefined! Not declared in Parent

βœ… Correct:

fragment Parent_user on User {
  email  // Explicitly declare if Parent needs it
  ...Child_user
}
Anti-Pattern 3: Over-Fetching in Parents

❌ Wrong:

// Parent fetches everything "just in case"
fragment Parent_user on User {
  id
  name
  email
  phone
  address
  bio
  ...Child_user
}

βœ… Correct:

// Parent fetches only what it renders directly
fragment Parent_user on User {
  id
  name  // Only these two used in Parent's render
  ...Child_user  // Child declares its own needs
}

Detailed Examples

Example 1: Dashboard with Multiple Widgets

Scenario: A dashboard page displays user info in a header, activity feed in the main area, and settings panel in a sidebar. Each widget should manage its own data.

// DashboardPage.jsx
import {graphql, useLazyLoadQuery} from 'react-relay';

function DashboardPage() {
  const data = useLazyLoadQuery(
    graphql`
      query DashboardPageQuery {
        viewer {
          id  # Dashboard needs ID for analytics
          ...DashboardHeader_user
          ...ActivityFeed_user
          ...SettingsPanel_user
        }
      }
    `,
    {}
  );

  // Dashboard CANNOT access name, email, preferences, etc.
  // It only knows the viewer's ID and has opaque references
  console.log(data.viewer.id);  // βœ… Works
  console.log(data.viewer.name); // ❌ undefined

  return (
    <div className="dashboard">
      <DashboardHeader user={data.viewer} />
      <main>
        <ActivityFeed user={data.viewer} />
      </main>
      <aside>
        <SettingsPanel user={data.viewer} />
      </aside>
    </div>
  );
}
// DashboardHeader.jsx
function DashboardHeader({user}) {
  const data = useFragment(
    graphql`
      fragment DashboardHeader_user on User {
        name
        avatar
        memberSince
      }
    `,
    user
  );

  return (
    <header>
      <img src={data.avatar} alt={data.name} />
      <h1>Welcome, {data.name}</h1>
      <span>Member since {data.memberSince}</span>
    </header>
  );
}
// ActivityFeed.jsx
function ActivityFeed({user}) {
  const data = useFragment(
    graphql`
      fragment ActivityFeed_user on User {
        recentActivity(first: 10) {
          edges {
            node {
              id
              type
              timestamp
              description
            }
          }
        }
      }
    `,
    user
  );

  return (
    <ul>
      {data.recentActivity.edges.map(({node}) => (
        <li key={node.id}>
          {node.description} - {node.timestamp}
        </li>
      ))}
    </ul>
  );
}
// SettingsPanel.jsx
function SettingsPanel({user}) {
  const data = useFragment(
    graphql`
      fragment SettingsPanel_user on User {
        email
        preferences {
          notifications
          theme
          language
        }
      }
    `,
    user
  );

  return (
    <div>
      <h3>Settings</h3>
      <p>Email: {data.email}</p>
      <label>
        <input 
          type="checkbox" 
          checked={data.preferences.notifications} 
        />
        Notifications
      </label>
      {/* More settings... */}
    </div>
  );
}

Data Flow Diagram:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DashboardPage Query                         β”‚
β”‚ viewer { id }                               β”‚
β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
β”‚  β”‚ DashboardHeader  β”‚                      β”‚
β”‚  β”‚ name, avatar     β”‚                      β”‚
β”‚  β”‚ memberSince      β”‚                      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
β”‚  β”‚ ActivityFeed     β”‚                      β”‚
β”‚  β”‚ recentActivity { β”‚                      β”‚
β”‚  β”‚   type, time     β”‚                      β”‚
β”‚  β”‚ }                β”‚                      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
β”‚                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”‚
β”‚  β”‚ SettingsPanel    β”‚                      β”‚
β”‚  β”‚ email,           β”‚                      β”‚
β”‚  β”‚ preferences      β”‚                      β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Each widget is isolated - changes to one
don't affect others

Why this works:

  • DashboardPage doesn't know or care what data each widget needs
  • Widgets can be reordered, removed, or replaced without touching parent code
  • Each widget can evolve independently (e.g., ActivityFeed adds filtering)
  • Testing is simple: mock the fragment reference for each widget in isolation

Example 2: Nested Comment Thread

Scenario: A recursive comment component where each comment can have replies. Each level needs different data fields.

// CommentThread.jsx
function CommentThread({post}) {
  const data = useFragment(
    graphql`
      fragment CommentThread_post on Post {
        id
        commentCount  # Parent needs total count
        topLevelComments(first: 20) {
          edges {
            node {
              id
              ...Comment_comment
            }
          }
        }
      }
    `,
    post
  );

  return (
    <div>
      <h3>{data.commentCount} Comments</h3>
      {data.topLevelComments.edges.map(({node}) => (
        <Comment key={node.id} comment={node} depth={0} />
      ))}
    </div>
  );
}
// Comment.jsx (recursive)
function Comment({comment, depth}) {
  const data = useFragment(
    graphql`
      fragment Comment_comment on Comment {
        id
        author { name, avatar }
        body
        createdAt
        replies(first: 5) {
          edges {
            node {
              id
              ...Comment_comment  # Recursive fragment!
            }
          }
        }
      }
    `,
    comment
  );

  const [showReplies, setShowReplies] = useState(true);

  return (
    <div style={{marginLeft: depth * 20}}>
      <div className="comment-header">
        <img src={data.author.avatar} alt="" />
        <strong>{data.author.name}</strong>
        <time>{data.createdAt}</time>
      </div>
      <p>{data.body}</p>
      
      {data.replies.edges.length > 0 && (
        <button onClick={() => setShowReplies(!showReplies)}>
          {showReplies ? 'Hide' : 'Show'} {data.replies.edges.length} replies
        </button>
      )}
      
      {showReplies && data.replies.edges.map(({node}) => (
        <Comment 
          key={node.id} 
          comment={node} 
          depth={depth + 1} 
        />
      ))}
    </div>
  );
}

Boundary behavior in recursion:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ CommentThread (depth 0)             β”‚
β”‚ Knows: commentCount                 β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Comment A (depth 0)             β”‚ β”‚
β”‚ β”‚ Knows: author, body, createdAt  β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Comment A.1 (depth 1)       β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ Knows: author, body, time   β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Comment A.1.a (depth 2)β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Same fragment, new ref β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Each instance has its own masked data
even though they use the same fragment

Key insight: Even though Comment_comment is used recursively, each instance maintains its own data boundary. Comment A cannot access Comment A.1's author dataβ€”each must call useFragment independently.

Example 3: Conditional Data Boundaries

Scenario: An admin dashboard that shows different data based on user permissions. Boundaries prevent accidental data leaks.

// UserProfile.jsx
function UserProfile({user}) {
  const data = useFragment(
    graphql`
      fragment UserProfile_user on User {
        id
        name
        role
        ...PublicProfile_user
        ...AdminProfile_user @include(if: $isAdmin)
      }
    `,
    user
  );

  return (
    <div>
      <h1>{data.name}</h1>
      <PublicProfile user={data} />
      {data.role === 'ADMIN' && <AdminProfile user={data} />}
    </div>
  );
}
// PublicProfile.jsx - safe data all users can see
function PublicProfile({user}) {
  const data = useFragment(
    graphql`
      fragment PublicProfile_user on User {
        bio
        avatar
        publicPosts(first: 10) {
          edges { node { id, title } }
        }
      }
    `,
    user
  );

  return (
    <section>
      <img src={data.avatar} alt="" />
      <p>{data.bio}</p>
      <h3>Recent Posts</h3>
      {/* Render posts... */}
    </section>
  );
}
// AdminProfile.jsx - sensitive data only admins see
function AdminProfile({user}) {
  const data = useFragment(
    graphql`
      fragment AdminProfile_user on User {
        email  # Sensitive!
        ipAddress  # Very sensitive!
        loginHistory(first: 20) {
          edges {
            node { timestamp, location }
          }
        }
        accountFlags { suspended, verified, flagged }
      }
    `,
    user
  );

  return (
    <section className="admin-only">
      <h3>⚠️ Admin Data</h3>
      <p>Email: {data.email}</p>
      <p>Last IP: {data.ipAddress}</p>
      {/* More sensitive data... */}
    </section>
  );
}

Security benefit: Even if someone accidentally renders <AdminProfile user={data} /> without checking permissions, they'd pass an opaque reference that doesn't contain admin data unless the @include(if: $isAdmin) directive was true at query time. The boundary acts as a second line of defense.

Example 4: Optimistic Updates with Boundaries

Scenario: A like button that updates immediately while maintaining data boundaries.

// PostCard.jsx
function PostCard({post}) {
  const data = useFragment(
    graphql`
      fragment PostCard_post on Post {
        id
        title
        excerpt
        ...LikeButton_post
        ...ShareButton_post
      }
    `,
    post
  );

  return (
    <article>
      <h2>{data.title}</h2>
      <p>{data.excerpt}</p>
      <footer>
        <LikeButton post={data} />
        <ShareButton post={data} />
      </footer>
    </article>
  );
}
// LikeButton.jsx
function LikeButton({post}) {
  const data = useFragment(
    graphql`
      fragment LikeButton_post on Post {
        id
        likeCount
        viewerHasLiked
      }
    `,
    post
  );

  const [commitLike] = useMutation(graphql`
    mutation LikeButtonMutation($postId: ID!) {
      likePost(postId: $postId) {
        post {
          id
          likeCount
          viewerHasLiked
        }
      }
    }
  `);

  const handleLike = () => {
    commitLike({
      variables: {postId: data.id},
      optimisticResponse: {
        likePost: {
          post: {
            id: data.id,
            likeCount: data.likeCount + (data.viewerHasLiked ? -1 : 1),
            viewerHasLiked: !data.viewerHasLiked,
          },
        },
      },
    });
  };

  return (
    <button onClick={handleLike}>
      {data.viewerHasLiked ? '❀️' : '🀍'} {data.likeCount}
    </button>
  );
}

Why boundaries help here:

  • PostCard doesn't re-render when like count changes (it doesn't declare likeCount)
  • Only LikeButton re-renders, minimizing UI updates
  • ShareButton is completely unaffected
  • Optimistic update is scoped exactly to the data LikeButton declared
Render Optimization via Boundaries:

❌ Without boundaries (all data in parent):
  Like changes β†’ PostCard re-renders
              β†’ All children re-render
              β†’ Expensive!

βœ… With boundaries:
  Like changes β†’ Only LikeButton re-renders
              β†’ Parent & siblings unaffected
              β†’ Efficient!

Common Mistakes

❌ Mistake 1: Trying to Access Masked Data

Problem:

function Parent({user}) {
  const data = useFragment(
    graphql`
      fragment Parent_user on User {
        id
        ...Child_user
      }
    `,
    user
  );

  // ❌ This doesn't work!
  console.log(data.Child_user.email);  // undefined
  console.log(data.email);  // undefined
}

Why it fails: Fragment spreads create opaque references, not nested objects. The data is masked.

Solution: If Parent needs the data, declare it explicitly:

fragment Parent_user on User {
  id
  email  // βœ… Explicitly declare if Parent needs it
  ...Child_user
}

❌ Mistake 2: Passing Raw Data Instead of References

Problem:

function Parent({user}) {
  const data = useFragment(parentFragment, user);
  const childData = useFragment(childFragment, data);  // ❌ WRONG!
  return <Child data={childData} />;
}

Why it fails: You're unwrapping the child's data in the parent, violating the boundary.

Solution:

function Parent({user}) {
  const data = useFragment(parentFragment, user);
  return <Child user={data} />;  // βœ… Pass reference, let Child unwrap
}

function Child({user}) {
  const data = useFragment(childFragment, user);  // Child unwraps it
  return <div>{data.email}</div>;
}

❌ Mistake 3: Over-Fetching to "Help" Children

Problem:

// Parent trying to be "helpful"
fragment Parent_user on User {
  id
  name
  email  // Parent doesn't use this
  phone  // or this
  address  // or this - but fetching "just in case"
  ...Child_user
}

Why it's bad:

  • Wastes network bandwidth
  • Increases query complexity
  • Couples parent to child's needs
  • If Child changes its requirements, Parent has obsolete fields

Solution:

// Parent fetches only what it uses
fragment Parent_user on User {
  id
  name  // Parent renders this
  ...Child_user  // Child declares its own needs
}

❌ Mistake 4: Not Using Fragments at All

Problem:

// Query fetches everything at the top
query AppQuery {
  viewer {
    id
    name
    email
    phone
    avatar
    bio
    posts { id, title, body, likes }
    comments { id, text, createdAt }
    # ... 50 more fields
  }
}

// Then prop-drilling everywhere
<Header name={data.viewer.name} avatar={data.viewer.avatar} />
<Profile bio={data.viewer.bio} email={data.viewer.email} />

Why it's bad:

  • No boundaries = no data masking benefits
  • Tight coupling between all components
  • Impossible to optimize re-renders
  • Hard to test components in isolation

Solution:

query AppQuery {
  viewer {
    id  # Only what App itself needs
    ...Header_user
    ...Profile_user
  }
}

// Each component declares and unwraps its own data

⚠️ Mistake 5: Confusing Fragments with Props

Problem: Thinking of fragment spreads like prop spreading in React.

// This mental model is WRONG:
fragment Parent_user on User {
  ...Child_user  // ❌ NOT like {...childProps}
}
// Assuming Parent can now access Child's fields

Correct mental model: Fragment spreads are data access grants, not data copies.

// Think of it like:
fragment Parent_user on User {
  ...Child_user  // "I need data FOR Child, but can't see it myself"
}

The parent is requesting data on behalf of the child, but Relay enforces that only the child can access it.

πŸ”§ Debugging Boundary Issues

Symptom: Data is undefined in a component.

Diagnostic steps:

  1. Check fragment declaration - Did you declare the field?
// Is the field in YOUR fragment?
fragment MyComponent_user on User {
  email  // ← Is this line present?
}
  1. Check fragment usage - Are you calling useFragment?
const data = useFragment(MyComponent_user, userRef);
console.log(data.email);  // Now it should work
  1. Check parent spread - Did parent include your fragment?
// In parent:
fragment Parent_user on User {
  id
  ...MyComponent_user  // ← Is this line present?
}
  1. Check reference passing - Are you passing the right prop?
// Parent:
return <MyComponent user={data} />;  // Passing 'user'

// Child:
function MyComponent({user}) {  // Receiving 'user' - names must match!
  const data = useFragment(fragment, user);
}

Key Takeaways

🎯 Core Principles:

  1. Data masking is mandatory - Parents cannot access child fragment data, even if they wanted to
  2. Fragments create boundaries - Each fragment declares an independent, isolated data requirement
  3. Opaque references enforce boundaries - Fragment spreads produce references that only useFragment can unwrap
  4. Duplication is OK - Multiple fragments can request the same field; Relay deduplicates automatically
  5. Boundaries enable optimization - Relay can minimize re-renders because it knows exactly which components depend on which data

πŸ’‘ Practical Guidelines:

  • βœ… DO declare only the fields your component directly uses
  • βœ… DO pass fragment references to children, not raw data
  • βœ… DO let each component call useFragment for its own data
  • βœ… DO duplicate field declarations when multiple components need them
  • βœ… DO use @arguments when parents need to parameterize child queries
  • ❌ DON'T try to access data from spread fragments
  • ❌ DON'T over-fetch fields you don't use
  • ❌ DON'T unwrap child fragments in parent components
  • ❌ DON'T prop-drill fragment data

🧠 Mental Models:

Think of fragments as:

  • πŸ” Privacy walls between apartments (each component has its own space)
  • πŸ“¦ Sealed packages (you can pass them around but can't peek inside without the key)
  • 🎫 Access tickets (spreading a fragment is requesting data access, not getting the data)
  • πŸ—‚οΈ File permissions (read access must be explicitly granted per file/field)

Data flow pattern:

Query β†’ Parent.useFragment() β†’ Parent accesses own fields
                            β†’ Pass reference to Child
                                β†’ Child.useFragment() β†’ Child accesses own fields

πŸ“‹ Quick Reference Card

Concept Description Code Pattern
Data Masking Automatic hiding of fragment data from other components ...Child_user β†’ opaque ref
Opaque Reference Unreadable object produced by fragment spread data.Child_user β†’ can't access fields
Fragment Unwrapping Using useFragment to access masked data useFragment(frag, ref) β†’ real data
Boundary Invisible barrier preventing cross-component data access Parent can't see Child's fields
Deduplication Relay automatically merges duplicate field requests Multiple id declarations β†’ one fetch
@arguments Parameterize child fragments from parent ...Child @arguments(n: 10)

Boundary Enforcement Checklist:

  • βœ… Each component has its own fragment
  • βœ… Each component calls useFragment for its data
  • βœ… Pass references, not raw data
  • βœ… Declare exactly what you render
  • βœ… Trust Relay to optimize

πŸ“š Further Study


Next Steps: Now that you understand parent-child data boundaries, you're ready to explore Data Masking Architecture - the broader system that makes these boundaries possible and how Relay uses them to optimize your entire application.