Skip to content

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

The most straightforward approach uses React Query's enabled option combined with the refetch function:

jsx
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:

jsx
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:

jsx
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:

jsx
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:

jsx
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.

jsx
// ❌ 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

  1. Use descriptive query keys that reflect what you're fetching
  2. Handle loading and error states appropriately in your UI
  3. Consider caching behavior - manual queries still benefit from React Query's cache
  4. Use appropriate refetch options based on your specific needs
  5. Combine with state management for complex interaction patterns

When to Use Each Approach

ApproachBest ForConsiderations
enabled: false with refetchSimple manual triggersEasy to implement, good for most cases
queryClient.fetchQueryComplex error handling, direct result accessRequires manual state management
Conditional enabledDependent data fetchingCleaner 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.