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

Reading Data the Relay Way

Learn the correct hooks and patterns for data access in Relay components

Reading Data the Relay Way

Master Relay's declarative data-fetching patterns with free flashcards and spaced repetition practice. This lesson covers fragments, queries, GraphQL connections, and the relationship between components and their data requirementsβ€”essential concepts for building performant React applications with Relay.

Welcome to Relay Data Fetching πŸ’»

Relay revolutionizes how React applications fetch and manage data by introducing a declarative, component-centric approach. Unlike traditional REST APIs where you manually orchestrate fetching, caching, and state management, Relay treats data dependencies as first-class citizens. Each component declares exactly what data it needs, and Relay handles the restβ€”batching requests, normalizing responses, and keeping your UI in sync with your GraphQL server.

This "colocation" philosophy means your data requirements live right alongside the components that use them. No more hunting through action creators or reducer files to understand what data flows where. When you read a Relay component, you immediately see what data it needs. This lesson will teach you the core patterns that make this magic happen.

Core Concepts πŸ”Ί

Fragments: The Building Blocks

In Relay, a fragment is a reusable piece of a GraphQL query that declares data dependencies for a specific component. Think of fragments as "data contracts"β€”they specify exactly what fields a component needs from a particular GraphQL type.

Why Fragments Matter:

  • 🎯 Colocation: Data requirements live with the component code
  • ♻️ Reusability: Fragments compose into larger queries
  • πŸ”’ Type Safety: GraphQL schema validates your data shape
  • ⚑ Performance: Relay optimizes fetching automatically

A fragment definition looks like this:

const UserProfile = graphql`
  fragment UserProfile_user on User {
    id
    name
    email
    avatarUrl
    createdAt
  }
`;

This fragment says: "I need these five fields from a User object." The naming convention ComponentName_propName helps Relay track which component owns which data.

Fragment Container Pattern:

Relays binds fragments to React components using the useFragment hook:

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

function UserProfile({ userRef }) {
  const data = useFragment(
    graphql`
      fragment UserProfile_user on User {
        id
        name
        email
        avatarUrl
      }
    `,
    userRef
  );

  return (
    <div>
      <img src={data.avatarUrl} alt={data.name} />
      <h2>{data.name}</h2>
      <p>{data.email}</p>
    </div>
  );
}

Notice that userRef is not the actual dataβ€”it's a fragment reference (an opaque identifier). The useFragment hook "unwraps" this reference to give you the actual data fields.

Queries: The Entry Points

While fragments declare data needs, queries are the entry points that actually fetch data from your GraphQL server. Queries compose fragments together and specify query variables.

const ProfilePageQuery = graphql`
  query ProfilePageQuery($userId: ID!) {
    user(id: $userId) {
      ...UserProfile_user
    }
  }
`;

The ...UserProfile_user syntax spreads the fragment into the query. Relay automatically:

  1. Combines all fragments into one optimized network request
  2. Fetches the data from your GraphQL endpoint
  3. Normalizes the response into its cache
  4. Distributes fragment data to components

Loading Queries with useLazyLoadQuery:

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

function ProfilePage({ userId }) {
  const data = useLazyLoadQuery(
    graphql`
      query ProfilePageQuery($userId: ID!) {
        user(id: $userId) {
          ...UserProfile_user
        }
      }
    `,
    { userId }
  );

  return <UserProfile userRef={data.user} />;
}

The query executes when the component renders, suspending until data arrives (works with React Suspense).

Connections: Paginating Lists

GraphQL connections are Relay's standardized way to handle paginated lists. Instead of returning raw arrays, connections provide cursors for efficient pagination.

Connection Structure:

Connection Structure:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         PostsConnection          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  edges: [                        β”‚
β”‚    {                             β”‚
β”‚      cursor: "opaque_string"     β”‚  ← Position marker
β”‚      node: { Post object }       β”‚  ← Actual data
β”‚    },                            β”‚
β”‚    ...                           β”‚
β”‚  ]                               β”‚
β”‚  pageInfo: {                     β”‚
β”‚    hasNextPage: true             β”‚  ← More data?
β”‚    hasPreviousPage: false        β”‚
β”‚    startCursor: "..."            β”‚
β”‚    endCursor: "..."              β”‚
β”‚  }                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why Connections?

  • πŸ“„ Efficient Pagination: Fetch only what you need
  • πŸ”„ Bidirectional: Support forward and backward pagination
  • 🎯 Cursor-based: More reliable than offset pagination
  • πŸ”§ Standardized: Works consistently across all list types

Using usePaginationFragment:

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

function PostList({ queryRef }) {
  const {
    data,
    loadNext,
    loadPrevious,
    hasNext,
    isLoadingNext
  } = usePaginationFragment(
    graphql`
      fragment PostList_query on Query
      @refetchable(queryName: "PostListPaginationQuery")
      @argumentDefinitions(
        first: { type: "Int", defaultValue: 10 }
        after: { type: "String" }
      ) {
        posts(first: $first, after: $after)
        @connection(key: "PostList_posts") {
          edges {
            node {
              id
              title
              excerpt
            }
          }
        }
      }
    `,
    queryRef
  );

  return (
    <div>
      {data.posts.edges.map(edge => (
        <article key={edge.node.id}>
          <h3>{edge.node.title}</h3>
          <p>{edge.node.excerpt}</p>
        </article>
      ))}
      {hasNext && (
        <button
          onClick={() => loadNext(10)}
          disabled={isLoadingNext}
        >
          Load More
        </button>
      )}
    </div>
  );
}

Key Annotations:

  • @refetchable: Makes fragment refetchable with new variables
  • @argumentDefinitions: Declares pagination variables
  • @connection: Tells Relay to treat this as a paginated list
  • key: Stable identifier for this connection in the store
The Data Flow πŸ”„

Understanding how data flows through Relay components is crucial:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           RELAY DATA FLOW                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

    1️⃣ Component Renders
           β”‚
           ↓
    2️⃣ Query Executes (useLazyLoadQuery)
           β”‚
           ↓
    3️⃣ Network Request β†’ GraphQL Server
           β”‚
           ↓
    4️⃣ Response Normalized β†’ Relay Store
           β”‚
           ↓
    5️⃣ Fragment Data Extracted
           β”‚
           ↓
    6️⃣ Component Re-renders with Data
           β”‚
           ↓
    7️⃣ Changes? β†’ Subscribe to Updates

Relay maintains a normalized store where entities are cached by their global ID. When data arrives:

  1. Response is broken into individual records
  2. Each record is stored by its id field
  3. Queries and fragments reference these records
  4. Updates to records automatically propagate to all components using them

πŸ’‘ Pro Tip: Because of normalization, if two components request the same user data, Relay fetches it only once and shares the cached result.

Fragment Composition

Fragments compose naturallyβ€”parent components spread child fragments into their own:

// Child component
function UserAvatar({ userRef }) {
  const data = useFragment(
    graphql`
      fragment UserAvatar_user on User {
        avatarUrl
        name
      }
    `,
    userRef
  );
  return <img src={data.avatarUrl} alt={data.name} />;
}

// Parent component
function UserCard({ userRef }) {
  const data = useFragment(
    graphql`
      fragment UserCard_user on User {
        name
        email
        ...UserAvatar_user  # Spreads child fragment
      }
    `,
    userRef
  );

  return (
    <div>
      <UserAvatar userRef={data} />
      <h3>{data.name}</h3>
      <p>{data.email}</p>
    </div>
  );
}

Notice UserCard spreads UserAvatar_user but still accesses name and email directly. Each component gets exactly what it declared.

Composition Benefits:

  • 🧩 Modular: Components are self-contained
  • πŸ” Discoverable: Data needs are explicit
  • πŸ›‘οΈ Safe: Can't access undeclared fields
  • πŸ“¦ Portable: Copy component + fragment = works anywhere

Examples with Explanations 🎯

Example 1: Basic User Profile

Let's build a complete user profile with multiple child components:

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

function ProfilePicture({ userRef }) {
  const data = useFragment(
    graphql`
      fragment ProfilePicture_user on User {
        avatarUrl
        name
      }
    `,
    userRef
  );

  return (
    <div className="profile-pic">
      <img src={data.avatarUrl} alt={data.name} />
    </div>
  );
}

// ProfileStats.jsx
function ProfileStats({ userRef }) {
  const data = useFragment(
    graphql`
      fragment ProfileStats_user on User {
        followerCount
        followingCount
        postCount
      }
    `,
    userRef
  );

  return (
    <div className="stats">
      <span>{data.postCount} posts</span>
      <span>{data.followerCount} followers</span>
      <span>{data.followingCount} following</span>
    </div>
  );
}

// ProfileHeader.jsx (parent)
function ProfileHeader({ userRef }) {
  const data = useFragment(
    graphql`
      fragment ProfileHeader_user on User {
        name
        username
        bio
        ...ProfilePicture_user
        ...ProfileStats_user
      }
    `,
    userRef
  );

  return (
    <header>
      <ProfilePicture userRef={data} />
      <div className="info">
        <h1>{data.name}</h1>
        <p>@{data.username}</p>
        <p>{data.bio}</p>
      </div>
      <ProfileStats userRef={data} />
    </header>
  );
}

// ProfilePage.jsx (root)
import { useLazyLoadQuery } from 'react-relay';

function ProfilePage({ username }) {
  const data = useLazyLoadQuery(
    graphql`
      query ProfilePageQuery($username: String!) {
        user(username: $username) {
          ...ProfileHeader_user
        }
      }
    `,
    { username }
  );

  return <ProfileHeader userRef={data.user} />;
}

What's Happening:

  1. ProfilePage executes the query with username variable
  2. Query spreads ProfileHeader_user fragment
  3. ProfileHeader spreads two child fragments plus declares its own fields
  4. Relay combines all fragments into one network request
  5. Each component receives only the data it declared

The Generated Network Request:

query ProfilePageQuery($username: String!) {
  user(username: $username) {
    # From ProfileHeader
    name
    username
    bio
    # From ProfilePicture
    avatarUrl
    # From ProfileStats
    followerCount
    followingCount
    postCount
  }
}

Relay automatically deduplicates fields and sends one optimized query!

Example 2: Paginated Feed

A real-world feed with infinite scroll:

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

function FeedPost({ postRef }) {
  const data = useFragment(
    graphql`
      fragment FeedPost_post on Post {
        id
        title
        content
        createdAt
        author {
          name
          avatarUrl
        }
        likeCount
        commentCount
      }
    `,
    postRef
  );

  return (
    <article>
      <div className="author">
        <img src={data.author.avatarUrl} alt={data.author.name} />
        <span>{data.author.name}</span>
        <time>{new Date(data.createdAt).toLocaleDateString()}</time>
      </div>
      <h2>{data.title}</h2>
      <p>{data.content}</p>
      <footer>
        <span>❀️ {data.likeCount}</span>
        <span>πŸ’¬ {data.commentCount}</span>
      </footer>
    </article>
  );
}

function Feed({ queryRef }) {
  const {
    data,
    loadNext,
    hasNext,
    isLoadingNext
  } = usePaginationFragment(
    graphql`
      fragment Feed_query on Query
      @refetchable(queryName: "FeedPaginationQuery")
      @argumentDefinitions(
        count: { type: "Int", defaultValue: 10 }
        cursor: { type: "String" }
      ) {
        feed(first: $count, after: $cursor)
        @connection(key: "Feed_feed") {
          edges {
            node {
              id
              ...FeedPost_post
            }
          }
        }
      }
    `,
    queryRef
  );

  // Infinite scroll logic
  React.useEffect(() => {
    const handleScroll = () => {
      const bottom = window.innerHeight + window.scrollY >= 
                     document.body.offsetHeight - 500;
      if (bottom && hasNext && !isLoadingNext) {
        loadNext(10);
      }
    };

    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [hasNext, isLoadingNext, loadNext]);

  return (
    <div className="feed">
      {data.feed.edges.map(edge => (
        <FeedPost key={edge.node.id} postRef={edge.node} />
      ))}
      {isLoadingNext && <div className="spinner">Loading...</div>}
    </div>
  );
}

Pagination Flow:

  1. Initial render fetches first 10 posts
  2. User scrolls near bottom
  3. loadNext(10) called with count
  4. Relay fetches next page using endCursor from pageInfo
  5. New edges appended to existing list
  6. Component re-renders with expanded list

The @connection Directive: The key: "Feed_feed" tells Relay this is a stable list identity. When new pages load, Relay:

  • Appends new edges to existing ones
  • Doesn't duplicate items (by id)
  • Maintains scroll position
  • Updates pageInfo state
Example 3: Nested Data with Arguments

Fragments can accept arguments for flexible data fetching:

function UserPosts({ userRef, limit }) {
  const data = useFragment(
    graphql`
      fragment UserPosts_user on User
      @argumentDefinitions(
        count: { type: "Int", defaultValue: 5 }
        includePrivate: { type: "Boolean", defaultValue: false }
      ) {
        posts(
          first: $count
          includePrivate: $includePrivate
        ) {
          edges {
            node {
              id
              title
              publishedAt
              visibility
            }
          }
        }
      }
    `,
    userRef
  );

  return (
    <section>
      <h3>Recent Posts</h3>
      {data.posts.edges.map(edge => (
        <div key={edge.node.id}>
          <h4>{edge.node.title}</h4>
          <span>{edge.node.visibility}</span>
          <time>{edge.node.publishedAt}</time>
        </div>
      ))}
    </section>
  );
}

// Parent spreads with arguments
function UserProfile({ userRef }) {
  const data = useFragment(
    graphql`
      fragment UserProfile_user on User {
        name
        ...UserPosts_user @arguments(count: 10, includePrivate: true)
      }
    `,
    userRef
  );

  return (
    <div>
      <h2>{data.name}</h2>
      <UserPosts userRef={data} />
    </div>
  );
}

Argument Flow:

  1. UserPosts declares arguments with defaults
  2. Parent spreads fragment with @arguments(count: 10, ...)
  3. Relay passes these to the GraphQL query
  4. Server receives posts(first: 10, includePrivate: true)
  5. Response contains 10 posts including private ones

πŸ’‘ When to Use Arguments:

  • Different components need different amounts of data
  • Conditional field inclusion (feature flags)
  • Filtering or sorting options
  • Pagination parameters
Example 4: Refetching with Variables

Sometimes you need to re-fetch data with different parameters:

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

function SearchResults({ queryRef }) {
  const [data, refetch] = useRefetchableFragment(
    graphql`
      fragment SearchResults_query on Query
      @refetchable(queryName: "SearchResultsRefetchQuery")
      @argumentDefinitions(
        searchTerm: { type: "String!", defaultValue: "" }
        category: { type: "String" }
      ) {
        search(query: $searchTerm, category: $category) {
          edges {
            node {
              id
              title
              snippet
              category
            }
          }
        }
      }
    `,
    queryRef
  );

  const [searchInput, setSearchInput] = React.useState('');
  const [selectedCategory, setSelectedCategory] = React.useState(null);

  const handleSearch = () => {
    refetch(
      { searchTerm: searchInput, category: selectedCategory },
      { fetchPolicy: 'network-only' }
    );
  };

  return (
    <div>
      <input
        value={searchInput}
        onChange={e => setSearchInput(e.target.value)}
        placeholder="Search..."
      />
      <select onChange={e => setSelectedCategory(e.target.value)}>
        <option value="">All Categories</option>
        <option value="tech">Technology</option>
        <option value="science">Science</option>
      </select>
      <button onClick={handleSearch}>Search</button>

      <div className="results">
        {data.search.edges.map(edge => (
          <div key={edge.node.id}>
            <h3>{edge.node.title}</h3>
            <p>{edge.node.snippet}</p>
            <span className="category">{edge.node.category}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

Refetch Options:

  • fetchPolicy: 'store-or-network': Use cache if available (default)
  • fetchPolicy: 'network-only': Always fetch fresh data
  • fetchPolicy: 'store-only': Never hit network

When refetch is called:

  1. Component shows loading state
  2. New query executes with updated variables
  3. Store updates with new data
  4. Component re-renders with fresh results

Common Mistakes ⚠️

1. Accessing Undeclared Fields

❌ Wrong:

function UserCard({ userRef }) {
  const data = useFragment(
    graphql`
      fragment UserCard_user on User {
        name
      }
    `,
    userRef
  );

  // ERROR: email not declared in fragment!
  return <p>{data.email}</p>;
}

βœ… Right:

function UserCard({ userRef }) {
  const data = useFragment(
    graphql`
      fragment UserCard_user on User {
        name
        email  # Declare all fields you use
      }
    `,
    userRef
  );

  return <p>{data.email}</p>;
}

Why It Matters: Relay's type system prevents runtime errors. If you try to access undeclared fields, TypeScript/Flow will catch it at compile time.

2. Forgetting Fragment Naming Convention

❌ Wrong:

const fragment = graphql`
  fragment UserData on User {  # Generic name
    name
  }
`;

βœ… Right:

const fragment = graphql`
  fragment UserCard_user on User {  # ComponentName_propName
    name
  }
`;

Why: The naming convention helps Relay's compiler generate correct types and prevents collisions when multiple components use the same GraphQL type.

3. Passing Raw Data Instead of Fragment References

❌ Wrong:

function ParentComponent({ queryRef }) {
  const data = useFragment(parentFragment, queryRef);
  
  // Passing extracted data directly
  return <ChildComponent user={data.user.name} />;
}

βœ… Right:

function ParentComponent({ queryRef }) {
  const data = useFragment(parentFragment, queryRef);
  
  // Pass the fragment reference
  return <ChildComponent userRef={data.user} />;
}

function ChildComponent({ userRef }) {
  const data = useFragment(childFragment, userRef);
  return <div>{data.name}</div>;
}

Why: Fragment references preserve Relay's ability to track data dependencies and update components when data changes.

4. Missing @connection Key in Pagination

❌ Wrong:

fragment PostList_query on Query {
  posts(first: $count, after: $cursor) {
    edges { node { id } }
  }
}

βœ… Right:

fragment PostList_query on Query {
  posts(first: $count, after: $cursor)
  @connection(key: "PostList_posts") {  # Stable key required
    edges { node { id } }
  }
}

Why: Without @connection, Relay can't track the paginated list across refetches, causing duplicate items or lost data.

5. Not Handling Loading States

❌ Wrong:

function MyComponent() {
  const data = useLazyLoadQuery(query, variables);
  // No Suspense boundary = white screen until load
  return <div>{data.user.name}</div>;
}

βœ… Right:

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <MyComponent />
    </Suspense>
  );
}

function MyComponent() {
  const data = useLazyLoadQuery(query, variables);
  return <div>{data.user.name}</div>;
}

Why: Relay uses React Suspense. Without a Suspense boundary, your app might show nothing during data fetching.

Key Takeaways 🎯

πŸ“‹ Quick Reference Card

Concept Purpose Key Hook/API
Fragment Declare component data needs useFragment
Query Fetch data from GraphQL server useLazyLoadQuery
Connection Handle paginated lists usePaginationFragment
Refetch Re-fetch with new variables useRefetchableFragment
Fragment Ref Opaque data identifier Pass between components

🧠 Memory Device: "FQCR"

  • Fragments declare what you need
  • Queries fetch from the server
  • Connections handle pagination
  • References flow between components

βœ… Best Practices Checklist

  • βœ“ Use ComponentName_propName naming
  • βœ“ Colocate fragments with components
  • βœ“ Pass fragment references, not raw data
  • βœ“ Add @connection keys for pagination
  • βœ“ Wrap queries in Suspense boundaries
  • βœ“ Declare all fields you access
  • βœ“ Compose fragments hierarchically

πŸ’‘ Pro Tips for Success

πŸ”§ Try This: Fragment Colocation Exercise

Take an existing component that fetches data with REST or other methods. Refactor it to use Relay fragments:

  1. Identify all data the component uses
  2. Create a fragment declaring those fields
  3. Replace data prop with fragment reference
  4. Add useFragment hook
  5. Verify it works with Relay DevTools

This exercise cements the colocation pattern in your mind!

πŸ€” Did You Know?

Relay's compiler (the relay-compiler package) runs at build time and generates TypeScript/Flow types for all your fragments and queries. This means you get full autocomplete for data fieldsβ€”your editor knows exactly what fields exist on data.user based on your fragment definition!

🌍 Real-World Analogy

Think of Relay fragments like restaurant orders:

  • Each diner (component) writes their order (fragment)
  • The waiter (query) collects all orders
  • The kitchen (GraphQL server) prepares one optimized meal
  • Each diner receives exactly what they ordered
  • If someone changes their order (refetch), only their plate updates

This "declarative ordering" system prevents the chaos of everyone shouting at the kitchen individually!

πŸ“š Further Study

Official Documentation:

Congratulations! You now understand Relay's declarative data-fetching approach. Practice building components with fragments, and you'll soon appreciate how Relay eliminates entire categories of data-fetching bugs. Next, explore mutations to learn how Relay handles data updates! πŸš€