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.


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.