Skip to main content
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:
App.tsx
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:
queryKeys.ts
/**
 * 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

useLogin.ts
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

useCurrentMonth.ts
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:
Dashboard.tsx
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

useBatches.ts
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

useNoViewedAlerts.ts
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:
AppSidebar.tsx
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

useCreateBatch.ts
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

Build docs developers (and LLMs) love