Hooks vs Direct Functions
Overview
QueryOperations offers two ways to work with queries and mutations:
- React Hooks (
useQuery,useMutation) - For use in React components - Direct functions (
queries,mutations) - For use outside React components 
Both approaches work with TanStack React Query and share the same principles of caching, state management, and data handling.
useQuery vs queries
🎣 useQuery (React Hook)
The useQuery hook should be used inside React components and provides automatic state management, caching, and re-rendering.
Features
- ✅ Automatic state management (
isLoading,isError,isSuccess,data,error) - ✅ Automatic re-rendering when data changes
 - ✅ Automatic caching and synchronization
 - ✅ Automatic refetch based on configurations
 - ✅ Support for 
enabled,refetchInterval,staleTime, etc. - ❌ Can only be used inside React components
 - ❌ Follows React hooks rules
 
Usage Example
function UserProfile({ userId }: { userId: string }) {
  // TanStack hook with all states
  const { 
    data: user, 
    isLoading, 
    isError, 
    error,
    refetch 
  } = operations.useQuery('getUser', userId, {
    enabled: !!userId, // Conditional query
    staleTime: 5 * 60 * 1000, // 5 minutes
    refetchOnWindowFocus: true
  })
  if (isLoading) return <Skeleton />
  if (isError) return <Error message={error.message} />
  
  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => refetch()}>Refresh</button>
    </div>
  )
}
Available States
const query = operations.useQuery('getUser', userId)
// Loading states
query.isLoading      // true during initial load
query.isFetching     // true during any fetch (including refetch)
query.isRefetching   // true during refetch of existing data
// Success/error states
query.isSuccess      // true when data was loaded successfully
query.isError        // true when there was an error
query.error          // error object (if any)
// Data
query.data           // data returned by the query
query.dataUpdatedAt  // timestamp of last update
// Controls
query.refetch()      // manually refetch the query
query.remove()       // remove the query from cache
📦 queries (Direct Functions)
Functions in queries are the registered functions that can be called directly, outside React components.
Features
- ✅ Can be used anywhere (event handlers, utils, services)
 - ✅ Independent of React lifecycle
 - ✅ Useful for imperative calls
 - ❌ Does not provide automatic states
 - ❌ Does not cause re-rendering
 - ❌ Requires manual loading/error handling
 
Usage Example
// In a service, utility or event handler
async function exportUserData(userId: string) {
  try {
    // Call the function directly
    const user = await operations.queries.getUser(userId)
    
    // Process the data
    const csv = convertToCSV(user)
    downloadFile(csv, `user-${userId}.csv`)
    
    return { success: true }
  } catch (error) {
    console.error('Export error:', error)
    return { success: false, error }
  }
}
// In an event handler
async function handleQuickAction(userId: string) {
  const user = await operations.queries.getUser(userId)
  
  if (user.role === 'admin') {
    await operations.queries.getAdminPanel()
  }
}
// In a middleware or guard
async function checkUserPermissions(userId: string, resource: string) {
  const user = await operations.queries.getUser(userId)
  return user.permissions.includes(resource)
}
When to Use
Use queries directly when you need to:
- Fetch data in event handlers that don't immediately affect the UI
 - Perform validations or asynchronous checks
 - Process data outside React context
 - Integrate with non-React APIs (Node.js, tests, etc.)
 - Execute imperative logic based on data
 
useMutation vs mutations
🎣 useMutation (React Hook)
The useMutation hook manages the state of operations that modify data (POST, PUT, DELETE).
Features
- ✅ Mutation state management (
isLoading,isError,isSuccess) - ✅ Automatic callbacks (
onSuccess,onError,onSettled) - ✅ Integration with React Query cache (invalidation, optimistic updates)
 - ✅ Multiple calls tracking
 - ❌ Can only be used inside React components
 
Usage Example
function CreateUserForm() {
  const queryClient = useQueryClient()
  
  // Hook with state management
  const createUser = operations.useMutation('createUser', {
    onSuccess: (newUser) => {
      // Invalidate and refetch users query
      queryClient.invalidateQueries({ queryKey: ['getUsers'] })
      
      // Or update cache directly (optimistic update)
      queryClient.setQueryData(['getUsers'], (old: User[]) => [...old, newUser])
      
      toast.success('User created successfully!')
    },
    onError: (error) => {
      toast.error(`Error: ${error.message}`)
    },
    onSettled: () => {
      // Runs regardless of success or error
      console.log('Mutation completed')
    }
  })
  const handleSubmit = (data: CreateUserData) => {
    // Trigger the mutation
    createUser.mutate(data)
  }
  return (
    <form onSubmit={handleSubmit}>
      {/* ... form fields ... */}
      
      <button 
        type="submit" 
        disabled={createUser.isLoading}
      >
        {createUser.isLoading ? 'Creating...' : 'Create User'}
      </button>
      
      {createUser.isError && (
        <ErrorMessage error={createUser.error} />
      )}
    </form>
  )
}
Available States
const mutation = operations.useMutation('createUser')
// States
mutation.isIdle      // true before calling mutate
mutation.isLoading   // true during execution
mutation.isSuccess   // true after success
mutation.isError     // true after error
// Data
mutation.data        // returned data (after success)
mutation.error       // error (if any)
mutation.variables   // last parameters used
// Controls
mutation.mutate(data)       // execute the mutation
mutation.mutateAsync(data)  // version that returns Promise
mutation.reset()            // reset mutation state
📦 mutations (Direct Functions)
Functions in mutations execute data modification operations without automatic state management.
Features
- ✅ Can be used anywhere
 - ✅ Returns Promise directly
 - ✅ Useful for imperative or sequential calls
 - ❌ Does not provide automatic states
 - ❌ Does not invoke React Query callbacks
 - ❌ Requires manual cache management
 
Usage Example
// In a complex async function
async function bulkCreateUsers(usersData: CreateUserData[]) {
  const results = []
  
  for (const userData of usersData) {
    try {
      // Call the mutation directly
      const user = await operations.mutations.createUser(userData)
      results.push({ success: true, user })
    } catch (error) {
      results.push({ success: false, error, userData })
    }
  }
  
  return results
}
// In an onboarding process
async function completeOnboarding(data: OnboardingData) {
  // Sequence of mutations
  const user = await operations.mutations.createUser(data.user)
  const profile = await operations.mutations.createProfile({
    userId: user.id,
    ...data.profile
  })
  const preferences = await operations.mutations.updatePreferences({
    userId: user.id,
    ...data.preferences
  })
  
  return { user, profile, preferences }
}
// In a worker or background task
async function processQueue(queue: Task[]) {
  for (const task of queue) {
    await operations.mutations.processTask(task)
  }
}
When to Use
Use mutations directly when you need to:
- Execute multiple mutations in sequence
 - Perform batch operations
 - Integrate with non-React logic
 - Don't need immediate visual feedback
 - Process data in the background
 - Use in tests or scripts
 
Recommended Patterns
1. Use Hooks in Components
// ✅ GOOD - Hook in component
function UserList() {
  const { data: users, isLoading } = operations.useQuery('getUsers')
  
  if (isLoading) return <Loading />
  return <List items={users} />
}
// ❌ AVOID - Direct function in component for display
function UserList() {
  const [users, setUsers] = useState([])
  const [loading, setLoading] = useState(true)
  
  useEffect(() => {
    operations.queries.getUsers().then(setUsers).finally(() => setLoading(false))
  }, [])
  
  // Loses benefits of cache, refetch, etc.
}
2. Use Direct Functions in Non-UI Logic
// ✅ GOOD - Direct function in utility
async function generateReport(userId: string) {
  const user = await operations.queries.getUser(userId)
  const orders = await operations.queries.getUserOrders(userId)
  
  return createPDF({ user, orders })
}
3. Combine When Necessary
// ✅ GOOD - Hook for UI, function for logic
function UserDashboard({ userId }: Props) {
  // Hook to display data
  const { data: user } = operations.useQuery('getUser', userId)
  
  const handleExport = async () => {
    // Direct function for imperative action
    const userData = await operations.queries.getUser(userId)
    exportToCSV(userData)
  }
  
  return (
    <div>
      <UserCard user={user} />
      <button onClick={handleExport}>Export</button>
    </div>
  )
}
4. Cache Invalidation with Mutations
function EditUserForm({ userId }: Props) {
  const queryClient = useQueryClient()
  
  const updateUser = operations.useMutation('updateUser', {
    onSuccess: () => {
      // Invalidate specific query
      queryClient.invalidateQueries({ 
        queryKey: operations.getQueryKey('getUser', userId) 
      })
      
      // Or invalidate all related queries
      queryClient.invalidateQueries({ 
        queryKey: ['getUser'] 
      })
    }
  })
  
  return <form onSubmit={(data) => updateUser.mutate(data)} />
}
Conclusion
Both approaches are powerful and complementary:
- Hooks → React components, reactive UI, automatic management
 - Functions → Business logic, imperative operations, flexibility
 
Choose based on context and specific needs of each situation. Often, you'll use both in the same project to leverage the best of both worlds.