Skip to main content

Overview

SIGEAC uses React Query (@tanstack/react-query) for all server data fetching, providing caching, synchronization, and optimistic updates.

Architecture

1

Custom Hooks

Data fetching logic lives in custom hooks in the /hooks directory
2

React Query

Hooks use useQuery for reads and useMutation for writes
3

Axios Instance

All requests go through a configured Axios instance with interceptors
4

Type Safety

Full TypeScript support with type definitions from /types

Query Pattern

Create a custom hook for each data fetching operation.

Basic Query

hooks/general/clientes/useGetClients.ts
"use client"

import axiosInstance from "@/lib/axios"
import { Client } from "@/types"
import { useQuery } from "@tanstack/react-query"

const fetchClients = async (company: string | undefined): Promise<Client[]> => {
  const { data } = await axiosInstance.get(`/${company}/clients`)
  return data
}

export const useGetClients = (company: string | undefined) => {
  return useQuery<Client[]>({
    queryKey: ["clients", company],
    queryFn: () => fetchClients(company),
    staleTime: 1000 * 60 * 2, // 2 minutes
    enabled: !!company,
  })
}
See: hooks/general/clientes/useGetClients.ts:1

Using the Query Hook

components/ClientList.tsx
import { useGetClients } from '@/hooks/general/clientes/useGetClients'
import { useCompanyStore } from '@/stores/CompanyStore'

export function ClientList() {
  const { selectedCompany } = useCompanyStore()
  const { data, isLoading, error, refetch } = useGetClients(selectedCompany?.slug)
  
  if (isLoading) return <LoadingSpinner />
  if (error) return <ErrorMessage error={error} />
  
  return (
    <div>
      <button onClick={() => refetch()}>Refresh</button>
      <ul>
        {data?.map(client => (
          <li key={client.id}>{client.name}</li>
        ))}
      </ul>
    </div>
  )
}

Query with Parameters

hooks/general/clientes/useGetClientById.ts
import axiosInstance from "@/lib/axios"
import { Client } from "@/types"
import { useQuery } from "@tanstack/react-query"

const fetchClientById = async (
  company: string,
  id: string
): Promise<Client> => {
  const { data } = await axiosInstance.get(`/${company}/clients/${id}`)
  return data
}

export const useGetClientById = (
  company: string | undefined,
  id: string | undefined
) => {
  return useQuery<Client>({
    queryKey: ["client", company, id],
    queryFn: () => fetchClientById(company!, id!),
    enabled: !!company && !!id,
  })
}

Mutation Pattern

Create mutations for data modification operations.

Create Mutation

actions/general/clientes/actions.ts
import axiosInstance from "@/lib/axios"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"

interface CreateClientSchema {
  name: string
  phone?: string
  email?: string
  dni: string
  dni_type: string
}

export const useCreateClient = () => {
  const queryClient = useQueryClient()

  const createMutation = useMutation({
    mutationFn: async ({
      company,
      data
    }: {
      company: string
      data: CreateClientSchema
    }) => {
      const response = await axiosInstance.post(`/${company}/clients`, data)
      return response.data
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['clients'] })
      toast("¡Creado!", {
        description: `¡El cliente se ha creado correctamente!`
      })
    },
    onError: (error) => {
      toast.error('Error', {
        description: `No se creó correctamente: ${error}`
      })
    },
  })

  return { createClient: createMutation }
}
See: actions/general/clientes/actions.ts:16

Update Mutation

actions/general/clientes/actions.ts
export const useUpdateClient = () => {
  const queryClient = useQueryClient()

  const updateMutation = useMutation({
    mutationFn: async ({ 
      id, 
      data, 
      company 
    }: { 
      id: string
      data: Partial<CreateClientSchema>
      company: string 
    }) => {
      await axiosInstance.patch(`/${company}/clients/${id}`, data)
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['clients'] })
      toast("¡Actualizado!", {
        description: "¡El cliente se ha actualizado correctamente!",
      })
    },
    onError: (error) => {
      toast.error("Error", {
        description: `Hubo un error al actualizar el cliente: ${error}`,
      })
    },
  })

  return { updateClient: updateMutation }
}
See: actions/general/clientes/actions.ts:75

Delete Mutation

actions/general/clientes/actions.ts
export const useDeleteClient = () => {
  const queryClient = useQueryClient()

  const deleteMutation = useMutation({
    mutationFn: async ({ id, company }: { id: string; company: string }) => {
      await axiosInstance.delete(`/${company}/clients/${id}`)
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['clients'] })
      toast.success("¡Eliminado!", {
        description: `¡El cliente ha sido eliminado correctamente!`
      })
    },
    onError: () => {
      toast.error("Error", {
        description: "¡Hubo un error al eliminar el cliente!"
      })
    },
  })

  return { deleteClient: deleteMutation }
}
See: actions/general/clientes/actions.ts:47

Using Mutations

components/ClientForm.tsx
import { useCreateClient } from '@/actions/general/clientes/actions'
import { useCompanyStore } from '@/stores/CompanyStore'
import { useForm } from 'react-hook-form'
import { useRouter } from 'next/navigation'

export function ClientForm() {
  const { selectedCompany } = useCompanyStore()
  const { createClient } = useCreateClient()
  const router = useRouter()
  const form = useForm()
  
  const onSubmit = async (data) => {
    try {
      await createClient.mutateAsync({
        company: selectedCompany?.slug!,
        data
      })
      router.push(`/${selectedCompany?.slug}/clients`)
    } catch (error) {
      console.error('Failed to create client', error)
    }
  }
  
  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* Form fields */}
      <button 
        type="submit" 
        disabled={createClient.isPending}
      >
        {createClient.isPending ? 'Creating...' : 'Create Client'}
      </button>
    </form>
  )
}

Axios Configuration

The Axios instance is configured with interceptors:
lib/axios.ts
import axios from 'axios'
import Cookies from 'js-cookie'

const axiosInstance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
  withCredentials: true,
  headers: {
    "skip_zrok_interstitial": true,
  },
})

// Request interceptor - Add auth token
axiosInstance.interceptors.request.use((config) => {
  const token = Cookies.get('auth_token')
  
  if (token) {
    const authHeader = token.startsWith('Bearer ') ? token : `Bearer ${token}`
    config.headers.Authorization = authHeader
  }
  
  return config
})

// Response interceptor - Handle 401 errors
axiosInstance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response && error.response.status === 401) {
      Cookies.remove('auth_token')
      Cookies.remove('jwt')
      
      if (typeof window !== 'undefined') {
        window.location.href = '/login?session=expired'
      }
    }
    
    return Promise.reject(error)
  }
)

export default axiosInstance
See: lib/axios.ts:1

Query Options

Stale Time

Control how long data is considered fresh:
useQuery({
  queryKey: ['clients'],
  queryFn: fetchClients,
  staleTime: 1000 * 60 * 5, // 5 minutes
})

Cache Time

Control how long unused data stays in cache:
useQuery({
  queryKey: ['clients'],
  queryFn: fetchClients,
  cacheTime: 1000 * 60 * 10, // 10 minutes
})

Enabled

Conditionally enable queries:
const { data } = useQuery({
  queryKey: ['client', id],
  queryFn: () => fetchClient(id!),
  enabled: !!id, // Only run if id exists
})

Refetch Intervals

Automatically refetch data:
useQuery({
  queryKey: ['dashboard'],
  queryFn: fetchDashboardData,
  refetchInterval: 30000, // Every 30 seconds
})

Error Handling

Query Errors

import { useGetClients } from '@/hooks/general/clientes/useGetClients'

export function ClientList() {
  const { data, isLoading, error, isError } = useGetClients(company)
  
  if (isError) {
    return (
      <div className="p-4 bg-destructive/10 text-destructive rounded-md">
        <h3 className="font-semibold">Error loading clients</h3>
        <p className="text-sm">{error.message}</p>
      </div>
    )
  }
  
  // ... rest of component
}

Mutation Errors

const { createClient } = useCreateClient()

const handleSubmit = async (data) => {
  try {
    await createClient.mutateAsync({ company, data })
  } catch (error) {
    if (axios.isAxiosError(error)) {
      if (error.response?.status === 422) {
        // Validation errors
        const errors = error.response.data.errors
        console.error('Validation errors:', errors)
      } else if (error.response?.status === 500) {
        // Server error
        console.error('Server error:', error.response.data.message)
      }
    }
  }
}

Loading States

Query Loading

import { useGetClients } from '@/hooks/general/clientes/useGetClients'
import { Skeleton } from '@/components/ui/skeleton'

export function ClientList() {
  const { data, isLoading } = useGetClients(company)
  
  if (isLoading) {
    return (
      <div className="space-y-2">
        <Skeleton className="h-12 w-full" />
        <Skeleton className="h-12 w-full" />
        <Skeleton className="h-12 w-full" />
      </div>
    )
  }
  
  return (
    <ul>
      {data?.map(client => <li key={client.id}>{client.name}</li>)}
    </ul>
  )
}

Mutation Loading

const { createClient } = useCreateClient()

<button 
  onClick={() => createClient.mutate({ company, data })}
  disabled={createClient.isPending}
>
  {createClient.isPending ? (
    <>
      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
      Creating...
    </>
  ) : (
    'Create Client'
  )}
</button>

Pagination

hooks/general/clientes/useGetClientsPaginated.ts
import { useQuery } from "@tanstack/react-query"
import axiosInstance from "@/lib/axios"

interface PaginatedResponse<T> {
  data: T[]
  current_page: number
  last_page: number
  per_page: number
  total: number
}

export const useGetClientsPaginated = (
  company: string | undefined,
  page: number = 1,
  perPage: number = 10
) => {
  return useQuery<PaginatedResponse<Client>>({
    queryKey: ["clients", company, page, perPage],
    queryFn: async () => {
      const { data } = await axiosInstance.get(
        `/${company}/clients?page=${page}&per_page=${perPage}`
      )
      return data
    },
    enabled: !!company,
    keepPreviousData: true, // Keep old data while fetching new page
  })
}
Using pagination:
import { useState } from 'react'
import { useGetClientsPaginated } from '@/hooks/general/clientes/useGetClientsPaginated'

export function PaginatedClientList() {
  const [page, setPage] = useState(1)
  const { data, isLoading } = useGetClientsPaginated(company, page)
  
  return (
    <div>
      <ul>
        {data?.data.map(client => (
          <li key={client.id}>{client.name}</li>
        ))}
      </ul>
      
      <div className="flex gap-2">
        <button 
          onClick={() => setPage(p => Math.max(1, p - 1))}
          disabled={page === 1}
        >
          Previous
        </button>
        
        <span>Page {page} of {data?.last_page}</span>
        
        <button 
          onClick={() => setPage(p => p + 1)}
          disabled={page === data?.last_page}
        >
          Next
        </button>
      </div>
    </div>
  )
}

Infinite Queries

import { useInfiniteQuery } from "@tanstack/react-query"

export const useGetClientsInfinite = (company: string | undefined) => {
  return useInfiniteQuery({
    queryKey: ["clients-infinite", company],
    queryFn: async ({ pageParam = 1 }) => {
      const { data } = await axiosInstance.get(
        `/${company}/clients?page=${pageParam}`
      )
      return data
    },
    getNextPageParam: (lastPage) => {
      return lastPage.current_page < lastPage.last_page
        ? lastPage.current_page + 1
        : undefined
    },
    enabled: !!company,
  })
}

Dependent Queries

Fetch data that depends on other data:
export function ArticleDetail({ id }) {
  // First, get the article
  const { data: article } = useQuery({
    queryKey: ['article', id],
    queryFn: () => fetchArticle(id),
  })
  
  // Then, get the batch (depends on article)
  const { data: batch } = useQuery({
    queryKey: ['batch', article?.batch_id],
    queryFn: () => fetchBatch(article!.batch_id),
    enabled: !!article?.batch_id, // Only run when we have batch_id
  })
  
  return (
    <div>
      <h1>{article?.part_number}</h1>
      <p>Batch: {batch?.name}</p>
    </div>
  )
}

Optimistic Updates

Update UI before server confirms:
export const useUpdateClientOptimistic = () => {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: async ({ id, data, company }) => {
      await axiosInstance.patch(`/${company}/clients/${id}`, data)
    },
    onMutate: async ({ id, data }) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({ queryKey: ['clients'] })
      
      // Snapshot previous value
      const previous = queryClient.getQueryData(['clients'])
      
      // Optimistically update
      queryClient.setQueryData(['clients'], (old: Client[]) =>
        old.map(client => 
          client.id === id ? { ...client, ...data } : client
        )
      )
      
      return { previous }
    },
    onError: (err, variables, context) => {
      // Rollback on error
      queryClient.setQueryData(['clients'], context?.previous)
    },
    onSettled: () => {
      // Refetch after error or success
      queryClient.invalidateQueries({ queryKey: ['clients'] })
    },
  })
}

Best Practices

Include all dependencies in query keys:
// ✗ Bad - missing dependencies
queryKey: ["clients"]

// ✓ Good - includes company
queryKey: ["clients", company]

// ✓ Good - includes all filters
queryKey: ["clients", company, { status, search }]
Only run queries when you have required data:
useQuery({
  queryKey: ['client', id],
  queryFn: () => fetchClient(id!),
  enabled: !!id, // Don't run if id is undefined
})
Always handle loading and error states:
const { data, isLoading, error } = useGetClients(company)

if (isLoading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
if (!data) return null

return <ClientList clients={data} />
Always invalidate related queries:
onSuccess: () => {
  queryClient.invalidateQueries({ queryKey: ['clients'] })
  queryClient.invalidateQueries({ queryKey: ['dashboard'] })
}

Next Steps

Custom Hooks

Learn to create reusable custom hooks

API Integration

Integrate with backend APIs

Build docs developers (and LLMs) love