Refetchable Fragments
Making fragments independently refetchable with @refetchable
Refetchable Fragments in Relay
Master Relay's data refetching patterns with free flashcards and spaced repetition practice. This lesson covers refetchable fragments, refetch queries, and fragment variables—essential concepts for building dynamic, interactive UIs with efficient data fetching in GraphQL applications.
Welcome to Refetchable Fragments 🔄
Welcome to one of Relay's most powerful features! Refetchable fragments allow you to reload specific parts of your component's data without refetching everything or navigating away from the page. Think of it as a surgical data refresh—you update exactly what you need, when you need it, keeping your UI responsive and your network usage minimal.
In this lesson, you'll learn how to make fragments refetchable, implement pagination, handle filtering and sorting, and master the patterns that make Relay applications feel lightning-fast and responsive.
Core Concepts 💡
What Makes a Fragment Refetchable?
A refetchable fragment is a fragment that can independently fetch new data based on updated variables. Unlike regular fragments that receive their data passively from parent queries, refetchable fragments generate their own refetch queries.
To make a fragment refetchable, you use the @refetchable directive:
fragment UserProfile_user on User
@refetchable(queryName: "UserProfileRefetchQuery") {
id
name
email
posts(first: 10) {
edges {
node {
title
}
}
}
}
The queryName parameter tells Relay what to name the generated refetch query. Relay creates this query automatically—you never write it yourself.
The Anatomy of Refetchable Fragments 🔍
Let's break down what makes refetchable fragments special:
| Component | Purpose | Required? |
|---|---|---|
| @refetchable directive | Marks fragment as independently refetchable | ✅ Yes |
| queryName | Names the auto-generated refetch query | ✅ Yes |
| id field | Identifies the object to refetch (for Node interface) | ✅ Usually yes |
| @argumentDefinitions | Declares variables the fragment accepts | ⚠️ For parameterized refetching |
Fragment Variables and Arguments 📝
Refetchable fragments can accept variables that control what data gets fetched. You define these with @argumentDefinitions:
fragment PostList_user on User
@refetchable(queryName: "PostListRefetchQuery")
@argumentDefinitions(
count: {type: "Int", defaultValue: 10}
orderBy: {type: "PostOrder", defaultValue: RECENT}
) {
id
posts(first: $count, orderBy: $orderBy) {
edges {
node {
id
title
createdAt
}
}
}
}
These variables let you refetch with different parameters—more posts, different sorting, filtered results—without changing your fragment definition.
The useRefetchableFragment Hook 🎣
In your React component, you use useRefetchableFragment instead of useFragment:
import { useRefetchableFragment, graphql } from 'react-relay';
function PostList({ userRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment PostList_user on User
@refetchable(queryName: "PostListRefetchQuery")
@argumentDefinitions(
count: {type: "Int", defaultValue: 10}
) {
id
posts(first: $count) {
edges {
node {
id
title
}
}
}
}
`,
userRef
);
const loadMore = () => {
refetch({ count: 20 }); // Fetch 20 posts instead of 10
};
return (
<div>
{data.posts.edges.map(edge => (
<div key={edge.node.id}>{edge.node.title}</div>
))}
<button onClick={loadMore}>Load More</button>
</div>
);
}
The hook returns:
- data: The current fragment data
- refetch: A function to trigger a refetch with new variables
💡 Key Insight: The refetch function returns the data in-flight as a suspense resource, so you can use <Suspense> boundaries to show loading states.
Detailed Refetching Patterns 🔄
Pattern 1: Simple Refetch with Updated Variables
The most basic pattern—refetch the same fragment with different parameters:
function UserPosts({ userRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment UserPosts_user on User
@refetchable(queryName: "UserPostsRefetchQuery")
@argumentDefinitions(
sortBy: {type: "PostSort", defaultValue: RECENT}
) {
id
username
posts(first: 10, sortBy: $sortBy) {
edges {
node {
id
title
likes
createdAt
}
}
}
}
`,
userRef
);
const sortByRecent = () => refetch({ sortBy: 'RECENT' });
const sortByPopular = () => refetch({ sortBy: 'POPULAR' });
return (
<div>
<div>
<button onClick={sortByRecent}>Recent</button>
<button onClick={sortByPopular}>Popular</button>
</div>
{data.posts.edges.map(edge => (
<article key={edge.node.id}>
<h3>{edge.node.title}</h3>
<span>❤️ {edge.node.likes}</span>
</article>
))}
</div>
);
}
What happens: When you click "Popular", Relay generates and executes a query like:
query UserPostsRefetchQuery($id: ID!, $sortBy: PostSort!) {
node(id: $id) {
...UserPosts_user @arguments(sortBy: $sortBy)
}
}
Relay automatically:
- Uses the
idfrom your data to identify which user to refetch - Applies the new
sortByvariable - Updates the store and re-renders your component
Pattern 2: Refetch with Suspense Boundaries
For better UX, wrap refetch operations in Suspense boundaries:
function PostList({ userRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment PostList_user on User
@refetchable(queryName: "PostListRefetchQuery")
@argumentDefinitions(
category: {type: "String"}
) {
id
posts(first: 20, category: $category) {
edges {
node {
id
title
}
}
}
}
`,
userRef
);
const [isPending, startTransition] = useTransition();
const filterByCategory = (category) => {
startTransition(() => {
refetch({ category });
});
};
return (
<div>
<select
onChange={(e) => filterByCategory(e.target.value)}
disabled={isPending}
>
<option value="">All</option>
<option value="tech">Tech</option>
<option value="design">Design</option>
</select>
<Suspense fallback={<Spinner />}>
{isPending && <div className="loading-overlay">Updating...</div>}
{data.posts.edges.map(edge => (
<Post key={edge.node.id} post={edge.node} />
))}
</Suspense>
</div>
);
}
💡 Pro Tip: Using startTransition keeps your UI responsive during refetches. The old data stays visible while new data loads in the background.
Pattern 3: Pagination with Refetchable Fragments
While Relay has specialized hooks for pagination (usePaginationFragment), you can implement cursor-based pagination with refetchable fragments:
function InfinitePostList({ userRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment InfinitePostList_user on User
@refetchable(queryName: "InfinitePostListRefetchQuery")
@argumentDefinitions(
count: {type: "Int", defaultValue: 10}
after: {type: "String"}
) {
id
posts(first: $count, after: $after) {
edges {
node {
id
title
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
userRef
);
const loadMore = () => {
const { endCursor } = data.posts.pageInfo;
refetch({
count: 10,
after: endCursor
});
};
return (
<div>
{data.posts.edges.map(edge => (
<div key={edge.node.id}>{edge.node.title}</div>
))}
{data.posts.pageInfo.hasNextPage && (
<button onClick={loadMore}>Load More</button>
)}
</div>
);
}
⚠️ Important: This refetches and replaces the data. For true infinite scroll that appends results, use usePaginationFragment instead.
Practical Examples 🛠️
Example 1: Search Filter with Debouncing
A real-world pattern combining refetchable fragments with search:
import { useState, useEffect } from 'react';
import { useRefetchableFragment, graphql } from 'react-relay';
import { useDebouncedCallback } from 'use-debounce';
function SearchableUserList({ queryRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment SearchableUserList_query on Query
@refetchable(queryName: "SearchableUserListRefetchQuery")
@argumentDefinitions(
searchTerm: {type: "String", defaultValue: ""}
first: {type: "Int", defaultValue: 20}
) {
users(search: $searchTerm, first: $first) {
edges {
node {
id
name
email
}
}
}
}
`,
queryRef
);
const [searchInput, setSearchInput] = useState('');
// Debounce refetch to avoid excessive network requests
const debouncedRefetch = useDebouncedCallback((term) => {
refetch({ searchTerm: term });
}, 300);
useEffect(() => {
debouncedRefetch(searchInput);
}, [searchInput, debouncedRefetch]);
return (
<div>
<input
type="text"
placeholder="Search users..."
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
/>
<Suspense fallback={<div>Searching...</div>}>
<ul>
{data.users.edges.map(edge => (
<li key={edge.node.id}>
{edge.node.name} ({edge.node.email})
</li>
))}
</ul>
</Suspense>
</div>
);
}
Key techniques:
- ✅ Debouncing prevents refetch on every keystroke
- ✅ Suspense shows loading state during search
- ✅ Fragment manages its own search state
- ✅ Parent component doesn't need to know about refetch logic
Example 2: Multi-Filter Product Catalog
Combining multiple filter criteria:
function ProductCatalog({ storeRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment ProductCatalog_store on Store
@refetchable(queryName: "ProductCatalogRefetchQuery")
@argumentDefinitions(
category: {type: "String"}
minPrice: {type: "Float"}
maxPrice: {type: "Float"}
inStock: {type: "Boolean", defaultValue: false}
sortBy: {type: "ProductSort", defaultValue: POPULAR}
) {
id
products(
category: $category
minPrice: $minPrice
maxPrice: $maxPrice
inStock: $inStock
sortBy: $sortBy
first: 50
) {
edges {
node {
id
name
price
stockCount
imageUrl
}
}
}
}
`,
storeRef
);
const [filters, setFilters] = useState({
category: null,
minPrice: null,
maxPrice: null,
inStock: false,
sortBy: 'POPULAR'
});
const updateFilters = (newFilters) => {
setFilters(prev => ({ ...prev, ...newFilters }));
refetch({ ...filters, ...newFilters });
};
return (
<div className="catalog">
<aside className="filters">
<select
value={filters.category || ''}
onChange={(e) => updateFilters({ category: e.target.value || null })}
>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<label>
<input
type="checkbox"
checked={filters.inStock}
onChange={(e) => updateFilters({ inStock: e.target.checked })}
/>
In Stock Only
</label>
<select
value={filters.sortBy}
onChange={(e) => updateFilters({ sortBy: e.target.value })}
>
<option value="POPULAR">Popular</option>
<option value="PRICE_LOW">Price: Low to High</option>
<option value="PRICE_HIGH">Price: High to Low</option>
</select>
</aside>
<main className="products">
<Suspense fallback={<div>Loading products...</div>}>
{data.products.edges.map(edge => (
<div key={edge.node.id} className="product-card">
<img src={edge.node.imageUrl} alt={edge.node.name} />
<h3>{edge.node.name}</h3>
<p>${edge.node.price}</p>
<p>{edge.node.stockCount > 0 ? '✅ In Stock' : '❌ Out of Stock'}</p>
</div>
))}
</Suspense>
</main>
</div>
);
}
Example 3: Real-time Refresh Button
Manual refresh for time-sensitive data:
function LiveStockTicker({ stockRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment LiveStockTicker_stock on Stock
@refetchable(queryName: "LiveStockTickerRefetchQuery") {
id
symbol
currentPrice
change
changePercent
lastUpdated
}
`,
stockRef
);
const [isRefreshing, setIsRefreshing] = useState(false);
const handleRefresh = () => {
setIsRefreshing(true);
refetch(
{}, // No variable changes, just refetch current state
{
fetchPolicy: 'network-only', // Force fresh data from server
onComplete: () => setIsRefreshing(false)
}
);
};
const changeColor = data.change >= 0 ? 'green' : 'red';
const changeIcon = data.change >= 0 ? '📈' : '📉';
return (
<div className="stock-ticker">
<h2>{data.symbol}</h2>
<div className="price">${data.currentPrice.toFixed(2)}</div>
<div style={{ color: changeColor }}>
{changeIcon} {data.change.toFixed(2)} ({data.changePercent.toFixed(2)}%)
</div>
<small>Last updated: {new Date(data.lastUpdated).toLocaleTimeString()}</small>
<button
onClick={handleRefresh}
disabled={isRefreshing}
>
{isRefreshing ? '🔄 Refreshing...' : '🔄 Refresh'}
</button>
</div>
);
}
💡 Fetch Policy Options:
store-or-network: Use cached data if available (default)network-only: Always fetch fresh data from serverstore-only: Never hit network, only use cache
Example 4: Conditional Refetching Based on User Action
Refetch different data based on tab selection:
function UserDashboard({ userRef }) {
const [data, refetch] = useRefetchableFragment(
graphql`
fragment UserDashboard_user on User
@refetchable(queryName: "UserDashboardRefetchQuery")
@argumentDefinitions(
showDrafts: {type: "Boolean", defaultValue: false}
showArchived: {type: "Boolean", defaultValue: false}
) {
id
name
posts(
includeDrafts: $showDrafts
includeArchived: $showArchived
first: 20
) {
edges {
node {
id
title
status
}
}
}
}
`,
userRef
);
const [activeTab, setActiveTab] = useState('published');
const switchTab = (tab) => {
setActiveTab(tab);
switch(tab) {
case 'published':
refetch({ showDrafts: false, showArchived: false });
break;
case 'drafts':
refetch({ showDrafts: true, showArchived: false });
break;
case 'archived':
refetch({ showDrafts: false, showArchived: true });
break;
case 'all':
refetch({ showDrafts: true, showArchived: true });
break;
}
};
return (
<div>
<h1>Welcome, {data.name}! 👋</h1>
<nav>
<button
onClick={() => switchTab('published')}
className={activeTab === 'published' ? 'active' : ''}
>
📄 Published
</button>
<button
onClick={() => switchTab('drafts')}
className={activeTab === 'drafts' ? 'active' : ''}
>
✏️ Drafts
</button>
<button
onClick={() => switchTab('archived')}
className={activeTab === 'archived' ? 'active' : ''}
>
📦 Archived
</button>
<button
onClick={() => switchTab('all')}
className={activeTab === 'all' ? 'active' : ''}
>
🗂️ All
</button>
</nav>
<Suspense fallback={<div>Loading posts...</div>}>
<ul>
{data.posts.edges.map(edge => (
<li key={edge.node.id}>
{edge.node.title} <span>({edge.node.status})</span>
</li>
))}
</ul>
</Suspense>
</div>
);
}
Common Mistakes ⚠️
Mistake 1: Forgetting the ID Field
❌ Wrong:
fragment UserProfile_user on User
@refetchable(queryName: "UserProfileRefetchQuery") {
# Missing id!
name
email
}
✅ Correct:
fragment UserProfile_user on User
@refetchable(queryName: "UserProfileRefetchQuery") {
id # Required for refetching!
name
email
}
Why: Relay needs the id field to know which specific object to refetch using the node(id: $id) pattern.
Mistake 2: Mutating Variables Object
❌ Wrong:
const [filters, setFilters] = useState({ category: 'all' });
const updateCategory = (category) => {
filters.category = category; // Mutation!
refetch(filters);
};
✅ Correct:
const [filters, setFilters] = useState({ category: 'all' });
const updateCategory = (category) => {
const newFilters = { ...filters, category }; // Immutable
setFilters(newFilters);
refetch(newFilters);
};
Why: React state should be treated as immutable. Always create new objects.
Mistake 3: Not Handling Pending States
❌ Wrong:
const handleRefetch = () => {
refetch({ category: 'tech' });
// UI doesn't show anything is loading
};
✅ Correct:
const [isPending, startTransition] = useTransition();
const handleRefetch = () => {
startTransition(() => {
refetch({ category: 'tech' });
});
};
// In JSX:
{isPending && <Spinner />}
Why: Users need feedback during data fetches. Use transitions or suspense boundaries.
Mistake 4: Refetching Too Frequently
❌ Wrong:
<input
onChange={(e) => refetch({ search: e.target.value })}
// Refetches on every keystroke!
/>
✅ Correct:
const debouncedRefetch = useDebouncedCallback(
(term) => refetch({ search: term }),
300
);
<input
onChange={(e) => debouncedRefetch(e.target.value)}
// Waits 300ms after typing stops
/>
Why: Excessive network requests waste resources and may trigger rate limits.
Mistake 5: Incorrect Variable Types in @argumentDefinitions
❌ Wrong:
@argumentDefinitions(
count: {type: "Number", defaultValue: 10} # Wrong type!
)
✅ Correct:
@argumentDefinitions(
count: {type: "Int", defaultValue: 10} # Use GraphQL types
)
Why: Variable types must match GraphQL schema types exactly (Int, String, Boolean, Float, etc.).
Mistake 6: Using Wrong Hook
❌ Wrong:
const data = useFragment(
graphql`
fragment MyComponent_data on User
@refetchable(queryName: "MyRefetchQuery") {
id
name
}
`,
dataRef
);
// Can't access refetch function!
✅ Correct:
const [data, refetch] = useRefetchableFragment(
graphql`
fragment MyComponent_data on User
@refetchable(queryName: "MyRefetchQuery") {
id
name
}
`,
dataRef
);
// Now you can use refetch
Why: The @refetchable directive requires useRefetchableFragment, not useFragment.
Key Takeaways 🎯
The @refetchable directive transforms regular fragments into independently refetchable components with their own query generation.
useRefetchableFragment hook returns both data and a refetch function, giving components control over their data fetching.
Fragment variables declared with
@argumentDefinitionsallow parameterized refetching—sorting, filtering, pagination, and more.Always include id field when using
@refetchableon object types that implement the Node interface.Combine with Suspense and transitions for smooth, responsive UX during refetches.
Debounce frequent refetches (like search inputs) to reduce network load and improve performance.
Fetch policies control caching behavior:
store-or-network(default),network-only(force fresh),store-only(cache only).Refetch replaces data, it doesn't append—use
usePaginationFragmentfor infinite scroll patterns.
Quick Reference Card 📋
🔄 Refetchable Fragment Cheat Sheet
| Concept | Syntax | Purpose |
|---|---|---|
| Make refetchable | @refetchable(queryName: "Name") |
Enable independent refetching |
| Define variables | @argumentDefinitions(var: {type: "Int"}) |
Accept parameters for refetch |
| Use in component | useRefetchableFragment(fragment, ref) |
Get data + refetch function |
| Trigger refetch | refetch({ newVar: value }) |
Fetch with updated variables |
| Force fresh data | refetch({}, { fetchPolicy: 'network-only' }) |
Bypass cache entirely |
| Smooth transitions | startTransition(() => refetch({})) |
Keep UI responsive |
| Required field | id |
Identify object to refetch |
Common Patterns:
- 🔍 Search: Debounce + refetch with search term
- 🎛️ Filters: Combine multiple variables in refetch
- 🔄 Refresh: Force network-only fetch
- 📑 Tabs: Refetch with different boolean flags
- ⏳ Loading: Wrap in Suspense or use transitions
Try This: Build a Filterable List 🔧
Practice what you've learned:
- Create a fragment with
@refetchableand@argumentDefinitionsfor filtering - Implement multiple filter controls (dropdown, checkbox, search)
- Add debouncing to search input
- Use
startTransitionfor smooth filter updates - Display loading states with Suspense
- Add a "Reset Filters" button
Bonus Challenge: Implement "Save Filter Preset" functionality that stores filter combinations in local state.
📚 Further Study
- Relay Official Docs: Refetchable Fragments
- Relay API Reference: useRefetchableFragment
- React Docs: useTransition Hook
🎉 Congratulations! You now understand how to make fragments refetchable, control data fetching with variables, and build dynamic, responsive UIs with Relay. Next up: explore pagination patterns with usePaginationFragment for even more powerful data management!