Tambo360 uses TanStack Query (formerly React Query) v5 for all server state management. It provides automatic caching, background refetching, optimistic updates, and error handling out of the box.
Why TanStack Query?
Automatic Caching Data is cached and reused across components automatically
Background Sync Stale data is refetched in the background
Optimistic Updates Update UI instantly before server confirms
Error Handling Built-in retry logic and error states
Setup
Query Client Configuration
The QueryClient is configured at the app root:
import { QueryClient , QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient ()
export const App : React . FC = () => {
return (
< QueryClientProvider client = { queryClient } >
< AuthProvider >
< Router >
< AppRoutes />
</ Router >
</ AuthProvider >
</ QueryClientProvider >
)
}
Location: apps/frontend/App.tsx:9
The QueryClientProvider must wrap all components that use TanStack Query hooks.
Query Keys
Query keys uniquely identify queries for caching and invalidation.
Centralized Query Keys
All query keys are defined in a centralized file:
/**
* Centralized Query Keys for TanStack Query
*
* Benefits:
* - Type-safe query key management
* - Easy invalidation patterns
* - Consistent naming across the app
*/
import { BatchFilters } from '@/src/types/batch'
import { GraphParams } from '@/src/types/dashboard'
// Base keys for each feature
export const baseKeys = {
auth: [ 'auth' ] as const ,
batch: [ 'batch' ] as const ,
product: [ 'product' ] as const ,
cost: [ 'cost' ] as const ,
dashboard: [ 'dashboard' ] as const ,
alert: [ 'alert' ] as const ,
} as const
// Auth related keys
export const authKeys = {
all: baseKeys . auth ,
login: [ ... baseKeys . auth , 'login' ] as const ,
currentUser: [ ... baseKeys . auth , 'currentUser' ] as const ,
logout: [ ... baseKeys . auth , 'logout' ] as const ,
changePassword: [ ... baseKeys . auth , 'changePassword' ] as const ,
} as const
// Batch related keys
export const batchKeys = {
all: baseKeys . batch ,
lists : () => [ ... baseKeys . batch , 'list' ] as const ,
filters : ( filters : BatchFilters ) =>
[ ... baseKeys . batch , 'filters' , filters ] as const ,
detail : ( id : string ) => [ ... baseKeys . batch , id ] as const ,
day : () => [ ... baseKeys . batch , 'today' ] as const ,
} as const
export const alertKeys = {
all: baseKeys . alert ,
lists : () => [ ... baseKeys . alert , 'list' ] as const ,
filters : ( range : string ) => [ ... baseKeys . alert , 'filters' , range ] as const ,
lasts : () => [ ... baseKeys . alert , 'lasts' ] as const ,
noViewed : () => [ ... baseKeys . alert , 'noViewed' ] as const ,
detail : ( id : string ) => [ ... baseKeys . alert , id ] as const ,
}
export const dashboardKeys = {
graph : ( params : GraphParams ) =>
[ ... baseKeys . dashboard , 'graph' , params ] as const ,
current : () => [ ... baseKeys . dashboard , 'current' ] as const ,
}
// Export all keys for easy access
export const queryKeys = {
auth: authKeys ,
batch: batchKeys ,
alert: alertKeys ,
dashboard: dashboardKeys ,
} as const
Location: apps/frontend/src/utils/queryKeys.ts
Query Key Hierarchy
['batch'] # All batches
['batch', 'list'] # Batch lists
['batch', 'filters', {...}] # Filtered batches
['batch', '123'] # Specific batch
['batch', 'today'] # Today's batches
useQuery - Fetching Data
useQuery is used for GET requests and data fetching.
Basic Pattern
import { useQuery } from '@tanstack/react-query'
import { queryKeys } from '@/src/utils/queryKeys'
import { getBatches } from '@/src/utils/api/batch.api'
export function useBatches ({ filters } : { filters : BatchFilters }) {
return useQuery ({
queryKey: queryKeys . batch . filters ( filters ),
queryFn : async () => {
const { data } = await getBatches ({ filters })
return data
},
staleTime: 5 * 60 * 1000 , // 5 minutes
refetchOnWindowFocus: false ,
})
}
Location: apps/frontend/src/hooks/batch/useBatches.ts
useQuery Options
Unique identifier for the query. Must be an array. queryKey : queryKeys . batch . filters ( filters )
Function that returns a Promise. Fetches the actual data. queryFn : async () => {
const { data } = await api . get ( '/batches' )
return data
}
Time in ms before data is considered stale. Stale data is refetched in the background. staleTime : 5 * 60 * 1000 // 5 minutes
Whether to refetch when window regains focus. refetchOnWindowFocus : false // Don't refetch on focus
Conditionally enable/disable the query. enabled : !! userId // Only run if userId exists
Using Query Data in Components
import { useBatches } from '@/src/hooks/batch/useBatches'
const BatchList = () => {
const { data , isPending , error , isError } = useBatches ({
filters: { status: 'active' }
})
if ( isPending ) {
return < LoadingSpinner />
}
if ( isError ) {
return < div > Error: { error . message } </ div >
}
return (
< div >
{ data . batches . map (( batch ) => (
< BatchCard key = { batch . id } batch = { batch } />
)) }
</ div >
)
}
Return Values
const {
data , // The data returned from queryFn (undefined while loading)
isPending , // True while loading for the first time
isLoading , // True while loading (including background refetches)
isError , // True if an error occurred
error , // The error object if isError is true
isSuccess , // True if query succeeded
refetch , // Manually trigger a refetch
status , // 'pending' | 'error' | 'success'
fetchStatus , // 'fetching' | 'paused' | 'idle'
} = useQuery ( ... )
useMutation - Updating Data
useMutation is used for POST, PUT, DELETE requests and side effects.
Basic Pattern
import { useMutation , useQueryClient } from '@tanstack/react-query'
import { loginUser } from '@/src/utils/api/auth.api'
import { queryKeys } from '@/src/utils/queryKeys'
export function useLogin () {
const queryClient = useQueryClient ()
return useMutation <
AxiosResponse < { user : User ; token : string } > , // Success response type
AxiosError < { message : string } > , // Error type
LoginData // Variables type
> ({
mutationFn : async ( values : LoginData ) => {
const { data } = await loginUser ( values )
return data
},
onSuccess : () => {
// Invalidate and refetch user data
queryClient . invalidateQueries ({
queryKey: queryKeys . auth . currentUser
})
},
})
}
Location: apps/frontend/src/hooks/auth/useLogin.ts
useMutation Options
Function that performs the mutation. Receives variables as argument. mutationFn : async ( values : LoginData ) => {
const { data } = await api . post ( '/auth/login' , values )
return data
}
Called when mutation succeeds. Use for invalidating queries or showing success messages. onSuccess : ( data , variables ) => {
queryClient . invalidateQueries ({ queryKey: queryKeys . batch . all })
toast . success ( 'Batch created successfully' )
}
Called when mutation fails. onError : ( error ) => {
toast . error ( error . response ?. data ?. message || 'An error occurred' )
}
Called before mutation. Use for optimistic updates. onMutate : async ( newBatch ) => {
// Cancel outgoing refetches
await queryClient . cancelQueries ({ queryKey: queryKeys . batch . lists () })
// Snapshot previous value
const previousBatches = queryClient . getQueryData ( queryKeys . batch . lists ())
// Optimistically update
queryClient . setQueryData ( queryKeys . batch . lists (), ( old ) => [ ... old , newBatch ])
return { previousBatches }
}
Using Mutations in Components
import { useCreateBatch } from '@/src/hooks/batch/useCreateBatch'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
const CreateBatchForm = () => {
const { mutateAsync , isPending } = useCreateBatch ()
const { register , handleSubmit } = useForm ()
const onSubmit = handleSubmit ( async ( data ) => {
try {
await mutateAsync ( data )
toast . success ( 'Lote creado exitosamente' )
} catch ( error ) {
toast . error ( 'Error al crear lote' )
}
})
return (
< form onSubmit = { onSubmit } >
< input { ... register ( 'name' ) } />
< button type = "submit" disabled = { isPending } >
{ isPending ? 'Creando...' : 'Crear Lote' }
</ button >
</ form >
)
}
Return Values
const {
mutate , // Trigger mutation (fire and forget)
mutateAsync , // Trigger mutation (returns Promise)
isPending , // True while mutation is in progress
isError , // True if mutation failed
error , // Error object
isSuccess , // True if mutation succeeded
data , // Data returned from mutation
reset , // Reset mutation state
} = useMutation ( ... )
Real-World Examples
Example 1: Fetching Dashboard Data
import { useQuery } from '@tanstack/react-query'
import { queryKeys } from '@/src/utils/queryKeys'
import { api } from '@/src/services/api'
export function useCurrentMonth () {
return useQuery ({
queryKey: queryKeys . dashboard . current (),
queryFn : async () => {
const { data } = await api . get ( '/dashboard/current-month' )
return data
},
staleTime: 2 * 60 * 1000 , // 2 minutes
})
}
Location: apps/frontend/src/hooks/dashboard/useCurrentMonth.ts
Usage:
import { useCurrentMonth } from '@/src/hooks/dashboard/useCurrentMonth'
import { StatCard } from '@/src/components/shared/StatCard'
const Dashboard = () => {
const { data , isPending } = useCurrentMonth ()
return (
< div className = "grid grid-cols-4 gap-4" >
< StatCard
title = "Queso Producido"
value = { data ?. data . actual . quesos }
unit = " Kg"
isPending = { isPending }
/>
</ div >
)
}
Location: apps/frontend/src/pages/Dashboard.tsx:9
Example 2: Fetching with Filters
import { BatchFilters } from '@/src/types/batch'
import { getBatches } from '@/src/utils/api/batch.api'
import { queryKeys } from '@/src/utils/queryKeys'
import { useQuery } from '@tanstack/react-query'
interface BatchesFilters {
filters : BatchFilters
}
export function useBatches ({ filters } : BatchesFilters ) {
return useQuery ({
queryKey: queryKeys . batch . filters ( filters ),
queryFn : async () => {
const { data } = await getBatches ({ filters })
return data
},
staleTime: 5 * 60 * 1000 ,
refetchOnWindowFocus: false ,
})
}
Location: apps/frontend/src/hooks/batch/useBatches.ts
Example 3: Alert Notifications
import { useQuery } from '@tanstack/react-query'
import { queryKeys } from '@/src/utils/queryKeys'
import { api } from '@/src/services/api'
interface UseNoViewedAlertsProps {
id : string // Establishment ID
}
export function useNoViewedAlerts ({ id } : UseNoViewedAlertsProps ) {
return useQuery ({
queryKey: queryKeys . alert . noViewed (),
queryFn : async () => {
const { data } = await api . get ( `/alerts/no-viewed/ ${ id } ` )
return data
},
refetchInterval: 30000 , // Refetch every 30 seconds
})
}
Location: apps/frontend/src/hooks/alerts/useNoViewedAlerts.ts
Usage in Sidebar:
import { useNoViewedAlerts } from '@/src/hooks/alerts/useNoViewedAlerts'
import { useAuth } from '@/src/context/AuthContext'
export function AppSidebar () {
const { user } = useAuth ()
const { data } = useNoViewedAlerts ({
id: user . establecimientos [ 0 ]. idEstablecimiento ,
})
return (
< Link to = "/tambo-engine" >
TamboEngine
{ data && data . cantidad > 0 && (
< span className = "badge" > { data . cantidad } </ span >
) }
</ Link >
)
}
Location: apps/frontend/src/components/layout/AppSidebar.tsx:22
Example 4: Creating a Batch
import { useMutation , useQueryClient } from '@tanstack/react-query'
import { createBatch } from '@/src/utils/api/batch.api'
import { queryKeys } from '@/src/utils/queryKeys'
export function useCreateBatch () {
const queryClient = useQueryClient ()
return useMutation ({
mutationFn : async ( batchData : CreateBatchData ) => {
const { data } = await createBatch ( batchData )
return data
},
onSuccess : () => {
// Invalidate all batch queries
queryClient . invalidateQueries ({
queryKey: queryKeys . batch . all
})
// Also invalidate dashboard data
queryClient . invalidateQueries ({
queryKey: queryKeys . dashboard . current ()
})
},
})
}
Location: apps/frontend/src/hooks/batch/useCreateBatch.ts
Cache Invalidation
Invalidating queries refetches them if they’re currently being used.
Invalidate by Key
import { useQueryClient } from '@tanstack/react-query'
import { queryKeys } from '@/src/utils/queryKeys'
const queryClient = useQueryClient ()
// Invalidate all batch queries
queryClient . invalidateQueries ({
queryKey: queryKeys . batch . all
})
// Invalidate specific batch
queryClient . invalidateQueries ({
queryKey: queryKeys . batch . detail ( '123' )
})
// Invalidate batch lists only
queryClient . invalidateQueries ({
queryKey: queryKeys . batch . lists ()
})
Invalidation Patterns
// After creating a batch
onSuccess : () => {
queryClient . invalidateQueries ({ queryKey: queryKeys . batch . all })
}
// After updating a batch
onSuccess : ( data , variables ) => {
queryClient . invalidateQueries ({ queryKey: queryKeys . batch . detail ( variables . id ) })
queryClient . invalidateQueries ({ queryKey: queryKeys . batch . lists () })
}
// After deleting a batch
onSuccess : () => {
queryClient . invalidateQueries ({ queryKey: queryKeys . batch . all })
queryClient . invalidateQueries ({ queryKey: queryKeys . dashboard . current () })
}
Best Practices
Use centralized query keys Always import query keys from queryKeys.ts for consistency
Set appropriate staleTime Longer staleTime reduces unnecessary refetches
Invalidate related queries When mutating, invalidate all affected queries
Handle loading states Always show loading UI when isPending is true
Use mutateAsync for error handling mutateAsync returns a Promise, making it easier to handle errors in forms:const onSubmit = async ( data ) => {
try {
await mutateAsync ( data )
toast . success ( 'Success!' )
} catch ( error ) {
toast . error ( 'Failed!' )
}
}
Don’t use refetchInterval for real-time data. Use WebSockets or Server-Sent Events instead.
Common Hooks Structure
All Tambo360 hooks follow a consistent structure:
apps/frontend/src/hooks/
├── auth/
│ ├── useLogin.ts # useMutation
│ ├── useLogout.ts # useMutation
│ └── useRegister.ts # useMutation
├── batch/
│ ├── useBatches.ts # useQuery
│ ├── useBatch.ts # useQuery (single)
│ ├── useCreateBatch.ts # useMutation
│ ├── useUpdateBatch.ts # useMutation
│ └── useDeleteBatch.ts # useMutation
├── dashboard/
│ ├── useCurrentMonth.ts # useQuery
│ └── useGraph.ts # useQuery
└── alerts/
├── useAlerts.ts # useQuery
├── useLastsAlerts.ts # useQuery
├── useNoViewedAlerts.ts # useQuery
└── useViewedAlert.ts # useMutation
Auth Context How auth integrates with React Query
API Services Backend API endpoints
TypeScript Types Type definitions for API responses
Form Handling Using mutations in forms
External Resources
TanStack Query Docs Official TanStack Query documentation
Query Keys Guide Best practices for query keys