Skip to main content

Hooks vs Direct Functions

Overview

QueryOperations offers two ways to work with queries and mutations:

  1. React Hooks (useQuery, useMutation) - For use in React components
  2. 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.

Decision rule

Choose based on execution context first:

  • React component rendering UI state -> hooks
  • non-React service, utility, workflow, or event pipeline -> direct functions

Do not choose hooks just because the endpoint is important. Choose hooks only when the runtime context is React and the UI needs reactive state.


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

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.