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:
- Combines all fragments into one optimized network request
- Fetches the data from your GraphQL endpoint
- Normalizes the response into its cache
- 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 listkey: 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:
- Response is broken into individual records
- Each record is stored by its
idfield - Queries and fragments reference these records
- 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:
ProfilePageexecutes the query withusernamevariable- Query spreads
ProfileHeader_userfragment ProfileHeaderspreads two child fragments plus declares its own fields- Relay combines all fragments into one network request
- 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:
- Initial render fetches first 10 posts
- User scrolls near bottom
loadNext(10)called with count- Relay fetches next page using
endCursorfrompageInfo - New edges appended to existing list
- 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
pageInfostate
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:
UserPostsdeclares arguments with defaults- Parent spreads fragment with
@arguments(count: 10, ...) - Relay passes these to the GraphQL query
- Server receives
posts(first: 10, includePrivate: true) - 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 datafetchPolicy: 'store-only': Never hit network
When refetch is called:
- Component shows loading state
- New query executes with updated variables
- Store updates with new data
- 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_propNamenaming - β Colocate fragments with components
- β Pass fragment references, not raw data
- β Add
@connectionkeys 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:
- Identify all data the component uses
- Create a fragment declaring those fields
- Replace data prop with fragment reference
- Add
useFragmenthook - 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:
- Relay Documentation - Fragments - Complete guide to fragment patterns
- Relay Documentation - Pagination - Deep dive into connection handling
- Relay Compiler - Type Generation - How Relay generates TypeScript types
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! π