Advanced Patterns
Master Suspense integration, error boundaries, and performance optimization strategies
Advanced Relay Patterns
Master advanced Relay patterns with free flashcards and spaced repetition practice. This lesson covers pagination strategies, optimistic updates, and connection managementβessential techniques for building performant GraphQL applications with Relay Modern. Whether you're scaling a real-time app or optimizing data fetching, these patterns will elevate your React development skills.
Welcome to Advanced Relay π»
Relay is Facebook's powerful GraphQL client designed specifically for React applications. While basic Relay usage gets you started with declarative data fetching, advanced patterns unlock the full potential of this framework. These patterns address real-world challenges: handling large datasets efficiently, updating the UI instantly before server responses, managing complex cache interactions, and structuring queries for optimal performance.
In this lesson, we'll dive deep into battle-tested patterns used by production applications at scale. You'll learn when and how to apply each pattern, understand their trade-offs, and avoid common pitfalls that can lead to stale data or degraded user experience.
Core Concepts in Depth π
1. Connection Pattern and Pagination π
The connection pattern is Relay's standardized way of handling lists and pagination. It provides a consistent interface for paginated data across your entire application.
Connection Structure: A connection consists of:
- edges: An array of edge objects, each containing a
node(the actual item) and acursor(position marker) - pageInfo: Metadata with
hasNextPage,hasPreviousPage,startCursor, andendCursor - totalCount: Optional total number of items
| Field | Type | Purpose |
|---|---|---|
| edges | Array | Container for nodes with cursors |
| node | Object | The actual data item |
| cursor | String | Opaque position identifier |
| pageInfo | Object | Pagination metadata |
Pagination Strategies:
Forward Pagination ("Load More"):
const {data, loadNext, hasNext, isLoadingNext} = usePaginationFragment(
graphql`
fragment PostList_user on User
@refetchable(queryName: "PostListPaginationQuery") {
posts(first: $count, after: $cursor)
@connection(key: "PostList_user_posts") {
edges {
node {
id
title
content
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
userRef
);
// Load 10 more items
const handleLoadMore = () => {
if (hasNext && !isLoadingNext) {
loadNext(10);
}
};
Backward Pagination ("Load Previous"): Useful for chat applications or infinite scroll up:
const {data, loadPrevious, hasPrevious} = usePaginationFragment(
graphql`
fragment Messages_conversation on Conversation
@refetchable(queryName: "MessagesPaginationQuery") {
messages(last: $count, before: $cursor)
@connection(key: "Messages_conversation_messages") {
edges {
node {
id
text
timestamp
}
}
}
}
`,
conversationRef
);
Bi-directional Pagination: Combine both approaches when users need to navigate in both directions (timeline views, document editors).
π‘ Tip: Always use the @connection directive with a unique key to ensure Relay properly merges paginated results in the store.
2. Optimistic Updates π
Optimistic updates provide instant UI feedback by immediately updating the cache before the server responds. This creates a snappy, responsive user experience, especially on slow networks.
When to Use Optimistic Updates:
- β Simple, predictable mutations (like/unlike, follow/unfollow)
- β Actions where the outcome is nearly certain
- β UI interactions that users expect to be instant
- β Complex calculations the client can't replicate
- β Operations with high failure rates
- β Security-sensitive actions requiring server validation
Implementation Patterns:
Basic Optimistic Response:
const [commit, isInFlight] = useMutation(graphql`
mutation LikePostMutation($input: LikePostInput!) {
likePost(input: $input) {
post {
id
likeCount
viewerHasLiked
}
}
}
`);
const handleLike = (postId, currentCount) => {
commit({
variables: {
input: {postId}
},
optimisticResponse: {
likePost: {
post: {
id: postId,
likeCount: currentCount + 1,
viewerHasLiked: true
}
}
}
});
};
Optimistic Updater (Complex Changes): When you need to modify multiple records or connections:
commit({
variables: {input: {userId, listId}},
optimisticUpdater: (store) => {
const user = store.get(userId);
const list = store.get(listId);
// Add user to list's connection
const connection = ConnectionHandler.getConnection(
list,
'UserList_members'
);
const edge = ConnectionHandler.createEdge(
store,
connection,
user,
'UserEdge'
);
ConnectionHandler.insertEdgeBefore(connection, edge);
// Update count optimistically
list.setValue(
(list.getValue('memberCount') || 0) + 1,
'memberCount'
);
}
});
Rollback on Error: Relay automatically reverts optimistic updates when mutations fail, but you can enhance error handling:
commit({
variables: {input},
optimisticResponse: {...},
onError: (error) => {
// Show user-friendly error message
showToast('Failed to update. Please try again.');
// Log for debugging
console.error('Mutation failed:', error);
}
});
π‘ Pro Tip: For critical data consistency, use optimistic updates with a loading indicator. Show the optimistic state but also indicate the request is in-flight until server confirmation arrives.
3. Store Updaters and Cache Management ποΈ
Store updaters give you direct control over Relay's normalized cache. This is powerful for scenarios where declarative fragments aren't sufficient.
The Relay Store: Relay maintains a normalized cache where each object is stored once by its unique ID. References between objects use these IDs, preventing data duplication and ensuring consistency.
Common Updater Patterns:
Adding Items to Connections:
const updater = (store) => {
const payload = store.getRootField('createPost');
const newPost = payload.getLinkedRecord('post');
const user = store.get(userId);
const connection = ConnectionHandler.getConnection(
user,
'PostList_user_posts'
);
if (connection) {
const edge = ConnectionHandler.createEdge(
store,
connection,
newPost,
'PostEdge'
);
ConnectionHandler.insertEdgeAfter(connection, edge);
}
};
Removing Items:
const updater = (store) => {
const deletedPostId = store.getRootField('deletePost')
.getValue('deletedPostId');
ConnectionHandler.deleteNode(
connection,
deletedPostId
);
// Also remove from store entirely
store.delete(deletedPostId);
};
Updating Scalar Fields:
const updater = (store) => {
const user = store.get(userId);
user.setValue(
user.getValue('followerCount') + 1,
'followerCount'
);
};
Creating Records Manually:
const updater = (store) => {
const newRecord = store.create(
`client:new-notification:${Date.now()}`,
'Notification'
);
newRecord.setValue('New message received!', 'text');
newRecord.setValue(Date.now(), 'timestamp');
// Link to user's notifications
const user = store.get(userId);
const notifications = user.getLinkedRecords('notifications') || [];
user.setLinkedRecords([newRecord, ...notifications], 'notifications');
};
Connection Filters: When working with filtered connections, specify the filter key:
const connection = ConnectionHandler.getConnection(
user,
'TaskList_user_tasks',
{status: 'ACTIVE'} // Filter arguments
);
4. Refetching and Polling Strategies π
Refetching loads fresh data from the server, while polling does this automatically at intervals. Both ensure data freshness but have different use cases.
Refetch Patterns:
Query Refetching:
const [queryRef, loadQuery] = useQueryLoader(MyQuery);
// Refetch with same variables
const handleRefresh = () => {
loadQuery({id: userId}, {fetchPolicy: 'network-only'});
};
// Refetch with new variables
const handleSearch = (term) => {
loadQuery({searchTerm: term});
};
Fragment Refetching:
const {data, refetch} = useRefetchableFragment(
graphql`
fragment UserProfile_user on User
@refetchable(queryName: "UserProfileRefetchQuery") {
name
email
lastActive
}
`,
userRef
);
// Refetch this fragment's data
const handleUpdate = () => {
refetch({}, {fetchPolicy: 'network-only'});
};
Polling for Real-time Updates:
const {
data,
refetch,
isRefetching
} = useRefetchableFragment(...);
// Poll every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
refetch({}, {fetchPolicy: 'network-only'});
}, 30000);
return () => clearInterval(interval);
}, [refetch]);
Smart Polling with Visibility API: Stop polling when tab is hidden to save resources:
useEffect(() => {
let interval;
const startPolling = () => {
interval = setInterval(() => {
refetch({}, {fetchPolicy: 'network-only'});
}, 30000);
};
const stopPolling = () => {
if (interval) clearInterval(interval);
};
const handleVisibilityChange = () => {
if (document.hidden) {
stopPolling();
} else {
startPolling();
refetch({}, {fetchPolicy: 'network-only'}); // Immediate refresh
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
startPolling();
return () => {
stopPolling();
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [refetch]);
Fetch Policies:
| Policy | Behavior | Use Case |
|---|---|---|
| store-or-network | Use cache if available, else fetch | Default, most cases |
| store-only | Never fetch, cache only | Offline mode |
| network-only | Always fetch, ignore cache | Force refresh |
| store-and-network | Return cache, then fetch | Show stale data fast |
5. Fragment Composition and Colocation π§©
Fragment colocation means defining data requirements alongside the components that use them. This is a core Relay principle that improves maintainability and prevents over-fetching.
Composing Fragments:
Parent Component:
function FeedScreen() {
const data = useLazyLoadQuery(
graphql`
query FeedScreenQuery {
viewer {
...FeedList_viewer
}
}
`,
{}
);
return <FeedList viewer={data.viewer} />;
}
Child Component:
function FeedList({viewer}) {
const data = useFragment(
graphql`
fragment FeedList_viewer on User {
posts(first: 10) @connection(key: "FeedList_viewer_posts") {
edges {
node {
id
...PostCard_post
}
}
}
}
`,
viewer
);
return data.posts.edges.map(({node}) => (
<PostCard key={node.id} post={node} />
));
}
Leaf Component:
function PostCard({post}) {
const data = useFragment(
graphql`
fragment PostCard_post on Post {
title
content
author {
name
avatar
}
createdAt
}
`,
post
);
return (
<div>
<h3>{data.title}</h3>
<p>{data.content}</p>
<Author name={data.author.name} avatar={data.author.avatar} />
</div>
);
}
Benefits of Fragment Composition:
- π― Each component owns its data requirements
- π§ Easy to modify without breaking other components
- π¦ Automatic dead code elimination (unused fragments are removed)
- π Single query fetches all nested data efficiently
- π§ͺ Components remain testable with mock data
Fragment Arguments: Pass variables down through fragments:
graphql`
fragment PostList_user on User
@argumentDefinitions(
count: {type: "Int", defaultValue: 10}
includeImages: {type: "Boolean!", defaultValue: false}
) {
posts(first: $count) {
edges {
node {
id
title
images @include(if: $includeImages) {
url
}
}
}
}
}
`
Practical Examples π οΈ
Example 1: Infinite Scroll Feed with Optimistic Likes
This example combines pagination, optimistic updates, and fragment composition:
import {graphql, usePaginationFragment, useMutation} from 'react-relay';
import {useCallback} from 'react';
function InfiniteFeed({queryRef}) {
const {
data,
loadNext,
hasNext,
isLoadingNext
} = usePaginationFragment(
graphql`
fragment InfiniteFeed_query on Query
@refetchable(queryName: "InfiniteFeedPaginationQuery") {
posts(first: $count, after: $cursor)
@connection(key: "InfiniteFeed_posts") {
edges {
node {
id
title
content
likeCount
viewerHasLiked
}
}
}
}
`,
queryRef
);
const [commitLike] = useMutation(graphql`
mutation InfiniteFeedLikeMutation($input: LikePostInput!) {
likePost(input: $input) {
post {
id
likeCount
viewerHasLiked
}
}
}
`);
const handleLike = useCallback((postId, currentCount, isLiked) => {
commitLike({
variables: {
input: {postId}
},
optimisticResponse: {
likePost: {
post: {
id: postId,
likeCount: isLiked ? currentCount - 1 : currentCount + 1,
viewerHasLiked: !isLiked
}
}
}
});
}, [commitLike]);
const handleScroll = useCallback((e) => {
const bottom = e.target.scrollHeight - e.target.scrollTop === e.target.clientHeight;
if (bottom && hasNext && !isLoadingNext) {
loadNext(10);
}
}, [loadNext, hasNext, isLoadingNext]);
return (
<div onScroll={handleScroll} style={{overflowY: 'auto', height: '100vh'}}>
{data.posts.edges.map(({node}) => (
<div key={node.id} style={{padding: '20px', borderBottom: '1px solid #eee'}}>
<h2>{node.title}</h2>
<p>{node.content}</p>
<button
onClick={() => handleLike(node.id, node.likeCount, node.viewerHasLiked)}
style={{
background: node.viewerHasLiked ? '#e74c3c' : '#ecf0f1',
color: node.viewerHasLiked ? 'white' : 'black'
}}
>
β€οΈ {node.likeCount}
</button>
</div>
))}
{isLoadingNext && <div style={{padding: '20px', textAlign: 'center'}}>Loading more...</div>}
</div>
);
}
Key Points:
- β
Uses
@connectiondirective for proper pagination merging - β Optimistic response provides instant UI feedback
- β Scroll detection triggers automatic loading
- β Like/unlike works seamlessly without server delay
Example 2: Real-time Notification System with Store Updates
Implementing a notification badge that updates via WebSocket:
import {graphql, useFragment, useMutation, useSubscription} from 'react-relay';
import {useEffect} from 'react';
function NotificationBadge({userRef}) {
const data = useFragment(
graphql`
fragment NotificationBadge_user on User {
id
unreadNotifications {
totalCount
}
}
`,
userRef
);
// Subscribe to new notifications
useSubscription({
subscription: graphql`
subscription NotificationBadgeSubscription($userId: ID!) {
notificationReceived(userId: $userId) {
notification {
id
text
createdAt
read
}
}
}
`,
variables: {userId: data.id},
updater: (store) => {
const payload = store.getRootField('notificationReceived');
const newNotification = payload.getLinkedRecord('notification');
const user = store.get(data.id);
const unreadConnection = user.getLinkedRecord('unreadNotifications');
// Increment count
const currentCount = unreadConnection.getValue('totalCount') || 0;
unreadConnection.setValue(currentCount + 1, 'totalCount');
// Add to notifications list
const notifications = user.getLinkedRecords('notifications') || [];
user.setLinkedRecords([newNotification, ...notifications], 'notifications');
}
});
const [markAllRead] = useMutation(graphql`
mutation NotificationBadgeMarkAllReadMutation($userId: ID!) {
markAllNotificationsRead(userId: $userId) {
user {
id
unreadNotifications {
totalCount
}
}
}
}
`);
const handleMarkAllRead = () => {
markAllRead({
variables: {userId: data.id},
optimisticUpdater: (store) => {
const user = store.get(data.id);
const unreadConnection = user.getLinkedRecord('unreadNotifications');
unreadConnection.setValue(0, 'totalCount');
}
});
};
const count = data.unreadNotifications.totalCount;
return (
<div style={{position: 'relative', cursor: 'pointer'}} onClick={handleMarkAllRead}>
π
{count > 0 && (
<span style={{
position: 'absolute',
top: '-8px',
right: '-8px',
background: '#e74c3c',
color: 'white',
borderRadius: '10px',
padding: '2px 6px',
fontSize: '12px'
}}>
{count > 99 ? '99+' : count}
</span>
)}
</div>
);
}
Key Points:
- β Subscription automatically updates cache via custom updater
- β Manual store manipulation increments counter
- β Optimistic update provides instant feedback on mark-as-read
- β Badge updates in real-time without polling
Example 3: Complex Search with Debounced Refetching
Searching with automatic refetching and proper loading states:
import {graphql, useLazyLoadQuery, useQueryLoader} from 'react-relay';
import {useState, useEffect, useCallback, useTransition} from 'react';
const SearchQuery = graphql`
query SearchQuery($searchTerm: String!, $filters: SearchFilters) {
search(term: $searchTerm, filters: $filters) {
results {
id
title
description
relevanceScore
}
totalCount
facets {
category
count
}
}
}
`;
function SearchScreen() {
const [queryRef, loadQuery] = useQueryLoader(SearchQuery);
const [searchTerm, setSearchTerm] = useState('');
const [filters, setFilters] = useState({});
const [isPending, startTransition] = useTransition();
// Debounce search input
useEffect(() => {
if (searchTerm.length < 2) return;
const timer = setTimeout(() => {
startTransition(() => {
loadQuery(
{searchTerm, filters},
{fetchPolicy: 'network-only'}
);
});
}, 300); // 300ms debounce
return () => clearTimeout(timer);
}, [searchTerm, filters, loadQuery]);
const handleSearchChange = useCallback((e) => {
setSearchTerm(e.target.value);
}, []);
const handleFilterChange = useCallback((filterKey, value) => {
setFilters(prev => ({
...prev,
[filterKey]: value
}));
}, []);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={handleSearchChange}
placeholder="Search..."
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '2px solid #3498db',
borderRadius: '8px'
}}
/>
{isPending && (
<div style={{padding: '10px', color: '#7f8c8d'}}>Searching...</div>
)}
{queryRef && (
<SearchResults queryRef={queryRef} onFilterChange={handleFilterChange} />
)}
</div>
);
}
function SearchResults({queryRef, onFilterChange}) {
const data = useLazyLoadQuery(SearchQuery, queryRef);
return (
<div style={{marginTop: '20px'}}>
<div style={{marginBottom: '20px'}}>
<strong>Found {data.search.totalCount} results</strong>
<div style={{display: 'flex', gap: '10px', marginTop: '10px'}}>
{data.search.facets.map(facet => (
<button
key={facet.category}
onClick={() => onFilterChange('category', facet.category)}
style={{
padding: '6px 12px',
background: '#ecf0f1',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
{facet.category} ({facet.count})
</button>
))}
</div>
</div>
{data.search.results.map(result => (
<div key={result.id} style={{
padding: '15px',
marginBottom: '10px',
background: '#f8f9fa',
borderRadius: '8px'
}}>
<h3>{result.title}</h3>
<p>{result.description}</p>
<small style={{color: '#7f8c8d'}}>Score: {result.relevanceScore}</small>
</div>
))}
</div>
);
}
Key Points:
- β Debouncing prevents excessive API calls during typing
- β
useTransitionprovides non-blocking loading states - β Filters trigger automatic refetch with new variables
- β Facets enable dynamic filtering based on results
Example 4: Batch Mutation with Rollback Strategy
Handling multiple mutations with proper error recovery:
import {graphql, useMutation} from 'react-relay';
import {useState} from 'react';
function TodoBatchUpdate({todos}) {
const [errors, setErrors] = useState([]);
const [commitUpdate] = useMutation(graphql`
mutation TodoBatchUpdateMutation($input: UpdateTodoInput!) {
updateTodo(input: $input) {
todo {
id
completed
completedAt
}
}
}
`);
const handleMarkAllComplete = async () => {
setErrors([]);
const failedIds = [];
// Execute mutations in parallel with Promise.all
const mutations = todos
.filter(todo => !todo.completed)
.map(todo => {
return new Promise((resolve) => {
commitUpdate({
variables: {
input: {
id: todo.id,
completed: true
}
},
optimisticResponse: {
updateTodo: {
todo: {
id: todo.id,
completed: true,
completedAt: new Date().toISOString()
}
}
},
onCompleted: () => resolve({success: true, id: todo.id}),
onError: (error) => {
failedIds.push(todo.id);
resolve({success: false, id: todo.id, error});
}
});
});
});
const results = await Promise.all(mutations);
// Handle failures
const failed = results.filter(r => !r.success);
if (failed.length > 0) {
setErrors(failed.map(f => `Failed to update todo ${f.id}`));
// Optional: Revert failed items
failed.forEach(f => {
commitUpdate({
variables: {
input: {
id: f.id,
completed: false
}
},
updater: (store) => {
const todo = store.get(f.id);
if (todo) {
todo.setValue(false, 'completed');
todo.setValue(null, 'completedAt');
}
}
});
});
}
};
return (
<div>
<button onClick={handleMarkAllComplete} style={{
padding: '10px 20px',
background: '#2ecc71',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer'
}}>
β Mark All Complete
</button>
{errors.length > 0 && (
<div style={{
marginTop: '10px',
padding: '10px',
background: '#fee',
border: '1px solid #e74c3c',
borderRadius: '4px',
color: '#c0392b'
}}>
<strong>Errors occurred:</strong>
<ul>
{errors.map((err, i) => <li key={i}>{err}</li>)}
</ul>
</div>
)}
</div>
);
}
Key Points:
- β Parallel execution improves performance
- β Individual error handling prevents cascade failures
- β Failed mutations can be rolled back manually
- β User receives clear feedback on partial failures
Common Mistakes and How to Avoid Them β οΈ
1. Forgetting @connection Directive
β Wrong:
fragment UserPosts_user on User {
posts(first: $count, after: $cursor) {
edges {
node { id title }
}
}
}
β Correct:
fragment UserPosts_user on User {
posts(first: $count, after: $cursor)
@connection(key: "UserPosts_user_posts") {
edges {
node { id title }
}
}
}
Why: Without @connection, Relay creates separate cache entries for each page, causing pagination to fail. The connection directive ensures proper merging.
2. Overly Complex Optimistic Responses
β Wrong:
optimisticResponse: {
createPost: {
post: {
id: 'temp-id',
title: input.title,
content: input.content,
author: {
id: userId,
name: currentUser.name,
followers: currentUser.followers + 1, // Complex calculation
posts: [...currentUser.posts, {/*...*/}] // Deep nesting
},
comments: [],
likes: calculateInitialLikes() // Function call
}
}
}
β Correct:
optimisticResponse: {
createPost: {
post: {
id: 'temp-id',
title: input.title,
content: input.content,
author: {
id: userId
},
commentCount: 0,
likeCount: 0
}
}
}
Why: Optimistic responses should only include fields that are immediately knowable. Let the server response populate complex or calculated fields. Keep it simple!
3. Missing Error Handling in Mutations
β Wrong:
commit({
variables: {input},
optimisticResponse: {...}
});
β Correct:
commit({
variables: {input},
optimisticResponse: {...},
onCompleted: (response, errors) => {
if (errors) {
console.error('Mutation errors:', errors);
showToast('Something went wrong');
} else {
showToast('Success!');
}
},
onError: (error) => {
console.error('Network error:', error);
showToast('Network error. Please try again.');
}
});
Why: Networks are unreliable. Always handle both GraphQL errors (in onCompleted) and network errors (in onError).
4. Polling Without Cleanup
β Wrong:
useEffect(() => {
setInterval(() => {
refetch({});
}, 5000);
}, [refetch]);
β Correct:
useEffect(() => {
const interval = setInterval(() => {
refetch({});
}, 5000);
return () => clearInterval(interval);
}, [refetch]);
Why: Without cleanup, intervals persist after component unmount, causing memory leaks and unnecessary network requests.
5. Incorrect Store Updater Patterns
β Wrong:
updater: (store) => {
const payload = store.getRootField('createComment');
const comment = payload.getLinkedRecord('comment');
// Trying to mutate the record directly
comment.content = 'Modified'; // Won't work!
// Missing null checks
const post = store.get(postId);
const comments = post.getLinkedRecords('comments');
post.setLinkedRecords([...comments, comment], 'comments');
}
β Correct:
updater: (store) => {
const payload = store.getRootField('createComment');
const comment = payload.getLinkedRecord('comment');
if (!comment) return; // Early return if missing
const post = store.get(postId);
if (!post) return;
const comments = post.getLinkedRecords('comments') || [];
post.setLinkedRecords([...comments, comment], 'comments');
// Update count
post.setValue(comments.length + 1, 'commentCount');
}
Why: Store records are immutable. Use setValue/setLinkedRecords methods. Always add null checksβmissing data is common during optimistic updates.
6. Using Wrong Pagination Direction
β Wrong (for chat messages):
// Using first/after for chat (newest messages appear at bottom)
messages(first: $count, after: $cursor) @connection(...) {
edges { node { text } }
}
β Correct:
// Using last/before for chat (load older messages upward)
messages(last: $count, before: $cursor) @connection(...) {
edges { node { text } }
}
Why: Chat UIs typically show newest messages at bottom and load older ones upward. Use last/before for this pattern. Use first/after for traditional "load more" at the bottom.
7. Not Using Fragment Arguments for Variations
β Wrong:
// Creating separate fragments for slight variations
fragment PostCardFull_post on Post { title content images author }
fragment PostCardCompact_post on Post { title author }
β Correct:
fragment PostCard_post on Post
@argumentDefinitions(
includeContent: {type: "Boolean!", defaultValue: true}
includeImages: {type: "Boolean!", defaultValue: true}
) {
title
content @include(if: $includeContent)
images @include(if: $includeImages) { url }
author { name }
}
Why: Fragment arguments reduce duplication and make components more flexible. Use @include and @skip directives to conditionally fetch fields.
Key Takeaways π―
π Quick Reference Card
| Pattern | When to Use | Key Hook/API |
|---|---|---|
| Pagination | Large lists, infinite scroll | usePaginationFragment + @connection |
| Optimistic Updates | Instant UI feedback | optimisticResponse in mutations |
| Store Updaters | Complex cache modifications | updater + ConnectionHandler |
| Refetching | Refresh stale data | useRefetchableFragment + @refetchable |
| Polling | Real-time updates without WebSocket | setInterval + refetch + cleanup |
| Fragment Composition | Component data requirements | useFragment + spread syntax |
Essential Directives:
@connection(key: "UniqueKey")- Merge paginated results@refetchable(queryName: "QueryName")- Enable refetching@argumentDefinitions()- Pass variables to fragments@include(if: $var)- Conditionally fetch fields@skip(if: $var)- Conditionally skip fields
Critical Don'ts:
- β Don't paginate without
@connection - β Don't skip error handling in mutations
- β Don't forget interval cleanup
- β Don't mutate store records directly
- β Don't over-optimize prematurely
Remember:
- Start simple - Use basic patterns first, add complexity only when needed
- Colocate data - Keep fragments with their components
- Handle errors - Networks fail; plan for it
- Test optimistic updates - Verify rollback behavior
- Profile performance - Use React DevTools and Relay DevTools to identify bottlenecks
π‘ Pro Tip: Enable the Relay DevTools browser extension to visualize your store, inspect queries, and debug cache issues in real-time.
π Further Study
- Relay Official Documentation - Comprehensive guides and API reference
- Relay Examples Repository - Production-ready code samples and patterns
- GraphQL Cursor Connections Specification - Deep dive into the connection pattern
π Congratulations! You've mastered advanced Relay patterns. These techniques will help you build fast, maintainable GraphQL applications that scale. Practice implementing these patterns in your projects, and you'll soon handle complex data requirements with confidence.