Triggering React Query with Button Clicks
Problem
When using React Query's useQuery
hook, data fetching typically happens automatically when your component mounts. However, many applications require more control - you might need to fetch data only when a user clicks a button or performs another specific action. This pattern is essential for:
- Search interfaces that fetch results on form submission
- Pagination controls that load new pages on click
- Refresh buttons that manually update data
- Dependent queries that require user input
Solutions
Method 1: Using enabled
and refetch
(Recommended)
The most straightforward approach uses React Query's enabled
option combined with the refetch
function:
import { useQuery } from '@tanstack/react-query'
const fetchUserData = async () => {
const response = await fetch('/api/users')
return response.json()
}
function UserList() {
const { data, isLoading, error, refetch } = useQuery({
queryKey: ['users'],
queryFn: fetchUserData,
enabled: false, // Disable automatic fetching
})
return (
<div>
<button onClick={() => refetch()} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Fetch Users'}
</button>
{error && <div>Error: {error.message}</div>}
{data && (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)}
</div>
)
}
INFO
The enabled: false
option prevents the query from running automatically. The refetch
function allows you to trigger the query manually when needed.
Method 2: Using queryClient.fetchQuery
for More Control
For scenarios where you need better error handling or want to work with the query results directly:
import { useQueryClient } from '@tanstack/react-query'
function SearchComponent() {
const queryClient = useQueryClient()
const [searchTerm, setSearchTerm] = useState('')
const [results, setResults] = useState([])
const [isLoading, setIsLoading] = useState(false)
const handleSearch = async () => {
try {
setIsLoading(true)
const data = await queryClient.fetchQuery({
queryKey: ['search', searchTerm],
queryFn: () => fetchResults(searchTerm),
})
setResults(data)
} catch (error) {
console.error('Search failed:', error)
} finally {
setIsLoading(false)
}
}
return (
<div>
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<button onClick={handleSearch} disabled={isLoading}>
{isLoading ? 'Searching...' : 'Search'}
</button>
{/* Display results */}
</div>
)
}
Method 3: Conditional Querying with State
For situations where you want to start fetching only after a certain condition is met:
function UserProfile({ userId }) {
const [shouldFetch, setShouldFetch] = useState(false)
const { data, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUserById(userId),
enabled: shouldFetch, // Only fetch when shouldFetch is true
})
return (
<div>
<button onClick={() => setShouldFetch(true)}>
Load Profile
</button>
{isLoading && <div>Loading profile...</div>}
{data && (
<div>
<h2>{data.name}</h2>
<p>{data.email}</p>
</div>
)}
</div>
)
}
Advanced Patterns
Refetch with Options
The refetch
function accepts options for advanced control:
const { refetch } = useQuery({
queryKey: ['data'],
queryFn: fetchData,
enabled: false,
})
// Advanced refetch usage
const handleRefetch = () => {
refetch({
throwOnError: true, // Throw error instead of just logging
cancelRefetch: false, // Don't cancel current request if one is in progress
})
}
Dependent Queries with Manual Triggering
Combine manual triggering with dependent data:
function UserProjects({ userId }) {
// First, fetch user data (could be manual or automatic)
const { data: user } = useQuery({
queryKey: ['user', userId],
queryFn: fetchUser,
})
// Then manually fetch projects only when user data is available
const { data: projects, refetch: fetchProjects } = useQuery({
queryKey: ['projects', userId],
queryFn: () => fetchUserProjects(userId),
enabled: false,
})
return (
<div>
<h2>{user?.name}'s Projects</h2>
<button onClick={fetchProjects} disabled={!user}>
Load Projects
</button>
{/* Display projects */}
</div>
)
}
Common Pitfalls
WARNING
When using enabled: false
, remember that the query won't automatically refetch when dependencies change. You'll need to manually call refetch
when your query key changes.
DANGER
Avoid wrapping useQuery
in click handlers. Hooks must be called at the top level of your component, not conditionally or inside event handlers.
// ❌ Wrong - hooks can't be called conditionally
const handleClick = () => {
const { data } = useQuery({...})
}
// ✅ Correct - call hook at top level, trigger manually
const { refetch } = useQuery({...})
const handleClick = () => refetch()
Best Practices
- Use descriptive query keys that reflect what you're fetching
- Handle loading and error states appropriately in your UI
- Consider caching behavior - manual queries still benefit from React Query's cache
- Use appropriate refetch options based on your specific needs
- Combine with state management for complex interaction patterns
When to Use Each Approach
Approach | Best For | Considerations |
---|---|---|
enabled: false with refetch | Simple manual triggers | Easy to implement, good for most cases |
queryClient.fetchQuery | Complex error handling, direct result access | Requires manual state management |
Conditional enabled | Dependent data fetching | Cleaner than manual refetch for dependent queries |
By understanding these patterns, you can effectively control when and how your React Query data fetching occurs, creating more responsive and user-friendly applications.