Gestión de Estado del Frontend
Gestión de Estado del Frontend
Versión: 1.0 Fecha: 2025-11-19 Proyecto: Algesta Dashboard React
Tabla de Contenidos
- Filosofía de Gestión de Estado
- React Query (Estado del Servidor)
- Zustand (Estado del Cliente)
- Gestión de Estado URL (nuqs)
- Gestión de Estado de Formularios (React Hook Form)
- Árbol de Decisión de Gestión de Estado
- Mejores Prácticas de Gestión de Estado
- Consideraciones de Rendimiento
- Depuración
Filosofía de Gestión de Estado
El Dashboard de Algesta sigue una separación clara entre diferentes tipos de estado:
Estado del Servidor vs Estado del Cliente
-
Estado del Servidor (React Query): Datos de la API que viven en el servidor
- Órdenes, usuarios, activos, proveedores, etc.
- Cacheados, sincronizados e invalidados
- Gestionados por React Query
-
Estado del Cliente (Zustand): Estado de UI y datos temporales que viven solo en el cliente
- Preferencias de usuario, estado de UI, items seleccionados
- Persistidos en localStorage cuando sea necesario
- Gestionados por Zustand
Justificación
Diferentes tipos de estado requieren diferentes soluciones:
- Datos del servidor necesitan caching, sincronización e invalidación
- Estado del cliente necesita actualizaciones simples y persistencia opcional
- Estado URL habilita URLs compartibles y navegación del navegador
- Estado de formularios necesita validación y manejo de envíos
React Query (Estado del Servidor)
React Query (@tanstack/react-query) gestiona todo el estado del servidor en la aplicación.
Configuración
La configuración está definida en src/lib/react-query.ts (fuente canónica):
export const queryConfig = { queries: { refetchOnWindowFocus: false, // No refrescar al enfocar ventana retry: false, // No reintentar solicitudes fallidas staleTime: 1000 * 60, // Los datos están frescos por 1 minuto },};Nota: Esta configuración es la única fuente de verdad para los valores predeterminados globales de React Query. Cualquier cambio en el comportamiento de las consultas debe hacerse aquí.
Configuraciones Explicadas:
refetchOnWindowFocus: false- Previene refrescamiento automático cuando la ventana recupera el foco (reduce llamadas API)retry: false- Las solicitudes fallidas no se reintentarán automáticamente (manejo de errores más claro)staleTime: 1000 * 60- Los datos se consideran frescos por 1 minuto (reduce refrescamientos innecesarios)
Patrón de Consulta
Paso 1: Función de Llamada a la API
Crear función de llamada a la API en Funcionalidades/[x]/api/get-items.ts:
// Patrón de ejemplo (simplificado) - adaptar a tu funcionalidad específicaimport { API_CLIENT } from "@/lib/api-client";import type { ApiResponse, ListOrder, Pagination } from "@/types/api";
export interface OrderFilters { status?: string; priority?: string; page?: number; limit?: number;}
export const getOrders = async ( params: OrderFilters): Promise<ApiResponse<ListOrder[], Pagination>> => { const response = await API_CLIENT.get("/api/orders", { params }); return response.data;};Nota: Esto demuestra el patrón. Reemplaza OrderFilters, ListOrder y el Endpoint con tus tipos reales y Endpoints de API. Consulta los módulos de API de funcionalidades (ej., src/Funcionalidades/orders/api/*.ts) para implementaciones reales.
Paso 2: Hook de React Query
Crear hook de React Query en Funcionalidades/[x]/hooks/use-items.ts:
import { useQuery } from "@tanstack/react-query";import { getOrders } from "../api/get-orders";import type { OrderFilters } from "../api/get-orders";
export const useOrders = (filters: OrderFilters) => { return useQuery({ queryKey: ["orders", filters], queryFn: () => getOrders(filters), });};Paso 3: Usar en Componente
import { useOrders } from './hooks/use-orders';
const OrdersList = () => { const { data, isLoading, error } = useOrders({ status: 'pending' });
if (isLoading) return <Spinner />; if (error) return <div>Error al cargar órdenes</div>;
return ( <div> {data?.items.map(order => ( <OrderCard key={order.id} order={order} /> ))} </div> );};Patrón de Mutación
Paso 1: Función de Llamada a la API
Crear función de mutación en Funcionalidades/[x]/api/patch-item.ts:
import { API_CLIENT } from "@/lib/api-client";
export interface UpdateOrderDto { title?: string; description?: string; status?: string;}
export const updateOrder = async (id: string, data: UpdateOrderDto) => { const response = await API_CLIENT.patch(`/api/orders/${id}`, data); return response.data;};Paso 2: Hook de Mutación de React Query
import { useMutation, useQueryClient } from "@tanstack/react-query";import { updateOrder } from "../api/patch-order";import { toast } from "sonner";
export const useUpdateOrder = () => { const queryClient = useQueryClient();
return useMutation({ mutationFn: ({ id, data }: { id: string; data: UpdateOrderDto }) => updateOrder(id, data), onSuccess: () => { // Invalidar y refrescar lista de órdenes queryClient.invalidateQueries({ queryKey: ["orders"] }); toast.success("Orden actualizada exitosamente"); }, onError: (error) => { toast.error("Error al actualizar orden"); }, });};Paso 3: Usar en Componente
import { useUpdateOrder } from './hooks/use-update-order';
const UpdateOrderForm = ({ orderId }: Props) => { const { mutate: updateOrder, isPending } = useUpdateOrder();
const handleSubmit = (data: UpdateOrderDto) => { updateOrder({ id: orderId, data }); };
return ( <form onSubmit={handleSubmit}> {/* Campos de formulario */} <Button type="submit" disabled={isPending}> {isPending ? 'Actualizando...' : 'Actualizar Orden'} </Button> </form> );};Convención de Claves de Consulta
Estructura de claves de consulta consistente:
| Patrón | Ejemplo | Uso |
|---|---|---|
| Clave base | ['orders'] | Todas las órdenes |
| Con filtros | ['orders', { Estado: 'Pendiente' }] | Órdenes filtradas |
| Elemento único | ['orders', '123'] | Orden específica |
| Recursos anidados | ['orders', '123', 'assets'] | Activos de la orden |
Beneficios:
- Invalidación predecible
- Fácil de apuntar a consultas específicas
- Compartición automática de caché
Invalidación de Caché
Invalidar Todas las Órdenes:
queryClient.invalidateQueries({ queryKey: ["orders"] });Invalidar Orden Específica:
queryClient.invalidateQueries({ queryKey: ["orders", orderId] });Actualización Optimista:
const { mutate } = useMutation({ mutationFn: updateOrder, onMutate: async (newOrder) => { // Cancelar refrescamientos salientes await queryClient.cancelQueries({ queryKey: ["orders", newOrder.id] });
// Capturar valor anterior const previousOrder = queryClient.getQueryData(["orders", newOrder.id]);
// Actualizar optimistamente queryClient.setQueryData(["orders", newOrder.id], newOrder);
return { previousOrder }; }, onError: (err, newOrder, context) => { // Revertir en caso de error queryClient.setQueryData(["orders", newOrder.id], context?.previousOrder); }, onSettled: (newOrder) => { // Refrescar después de la mutación queryClient.invalidateQueries({ queryKey: ["orders", newOrder.id] }); },});Herramientas de Desarrollo de React Query
Habilitadas en modo desarrollo para depuración:
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
<QueryClientProvider client={queryClient}> <App /> <ReactQueryDevtools initialIsOpen={false} /></QueryClientProvider>Funcionalidades:
- Ver todas las consultas y su estado
- Inspeccionar datos de consulta y caché
- Activar refrescamientos manualmente
- Ver mutaciones
- Monitorear estados de carga
Zustand (Estado del Cliente)
Zustand proporciona gestión de estado ligera para el estado del lado del cliente.
Patrón de Store
Estructura Básica de Store:
import { create } from "zustand";
export interface StoreState { // Propiedades de estado count: number;
// Acciones para actualizar estado increment: () => void; decrement: () => void; reset: () => void;}
const useStore = create<StoreState>((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), reset: () => set({ count: 0 }),}));
export default useStore;Stores Existentes
1. Store de Usuario (src/store/use-user-store.ts)
Propósito: Almacenar información del usuario autenticado globalmente
Archivo: src/store/use-user-store.ts (ver este archivo para la implementación autorizada)
Estado:
export interface UserStore { user: User | null; setStoreUser: (state: User | null) => void;}Implementación:
import { create } from "zustand";import { StorageKeys } from "@/config/constants";import type { User } from "@/types/api";
const useUserStore = create<UserStore>((set) => ({ user: null, setStoreUser: (state) => set(() => ({ user: state ? { ...state } : null })),}));
// Inicializar desde localStorageif (typeof window !== undefined) { try { const localUser = JSON.parse(localStorage.getItem(StorageKeys.User) ?? ""); useUserStore.setState({ user: localUser || null }); } catch { useUserStore.setState({ user: null }); }}
export default useUserStore;Campos Importantes: El tipo User real puede incluir campos adicionales más allá de la interfaz básica mostrada. Consulta src/types/api.ts para la definición completa del tipo User.
Uso:
import useUserStore from '@/store/use-user-store';
const UserProfile = () => { const { user, setStoreUser } = useUserStore();
return ( <div> <p>Bienvenido, {user?.name}</p> </div> );};Persistencia: Sincronizado con localStorage para persistencia de sesión
2. Store de Cotización (src/store/use-quotation-store.ts)
Propósito: Almacenar datos de cotización seleccionada durante el flujo de selección de proveedor
Archivo: src/store/use-quotation-store.ts
Uso: Estado temporal durante el proceso de selección de subasta/cotización
3. Store de Detalle de Cliente (src/store/use-detail-user-client-store.ts)
Propósito: Almacenar información detallada del cliente para vistas de admin/agente
Archivo: src/store/use-detail-user-client-store.ts
Uso: Cachear detalles del cliente al ver perfil de cliente para evitar refrescar
4. Store de Detalle de Proveedor (src/store/use-detail-user-provider-store.ts)
Propósito: Almacenar información detallada del proveedor para vistas de admin/agente
Archivo: src/store/use-detail-user-provider-store.ts
Uso: Cachear detalles del proveedor al ver perfil de proveedor
5. Store de Notificaciones (src/store/use-notification-store.ts)
Propósito: Gestionar estado de notificaciones y contadores de no leídas
Archivo: src/store/use-notification-store.ts
Uso: Mostrar insignia de notificaciones y gestionar lista de notificaciones
Nota: Para detalles completos de implementación de cada store, referirse a los archivos reales de store en el directorio src/store/.
Ejemplo de Uso de Store
import useUserStore from '@/store/use-user-store';
const UserProfile = () => { const { user, setStoreUser } = useUserStore();
const handleLogout = () => { setStoreUser(null); };
return ( <div> <p>Bienvenido, {user?.name}</p> <button onClick={handleLogout}>Cerrar Sesión</button> </div> );};Store con Persistencia
import { create } from "zustand";import { persist } from "zustand/middleware";
interface PreferencesStore { theme: "light" | "dark"; setTheme: (theme: "light" | "dark") => void;}
const usePreferencesStore = create<PreferencesStore>()( persist( (set) => ({ theme: "light", setTheme: (theme) => set({ theme }), }), { name: "preferences-storage", // clave de localStorage } ));Gestión de Estado URL (nuqs)
nuqs gestiona el estado de filtros y paginación en parámetros de consulta URL.
Beneficios
- URLs Compartibles: Los usuarios pueden compartir vistas filtradas/paginadas
- Navegación del Navegador: Los botones atrás/adelante funcionan con filtros
- Estado Persistente: Los filtros persisten a través de recargas de página
- Enlaces Profundos: Enlaces directos a vistas específicas
Patrón
Definición de Hook:
import { useQueryStates, parseAsString, parseAsInteger } from "nuqs";
export const useOrderFilterQueryState = () => { const [filters, setFilters] = useQueryStates({ status: parseAsString, priority: parseAsString, page: parseAsInteger.withDefault(1), limit: parseAsInteger.withDefault(10), });
return { filters, setFilters };};Uso en Componente:
import { useOrderFilterQueryState } from './hooks/use-order-filter-query-state';import { useOrders } from './hooks/use-orders';
const OrdersList = () => { const { filters, setFilters } = useOrderFilterQueryState(); const { data } = useOrders(filters);
const handleStatusChange = (status: string) => { setFilters({ status }); };
return ( <div> <OrdersListFilters filters={filters} onFiltersChange={setFilters} /> <DataTableServer data={data?.items} pagination={data?.pagination} /> </div> );};Ejemplo de URL:
/app/orders?status=pending&priority=high&page=2&limit=20Parseadores
| Parseador | Tipo | Ejemplo |
|---|---|---|
parseAsString | string | null | ?name=john |
parseAsInteger | number | null | ?page=2 |
parseAsBoolean | boolean | null | ?Activo=true |
parseAsArrayOf | array | null | ?ids=1,2,3 |
parseAsStringEnum | enum | null | ?Estado=Pendiente |
Valores Predeterminados
const [filters, setFilters] = useQueryStates({ page: parseAsInteger.withDefault(1), limit: parseAsInteger.withDefault(10), status: parseAsString.withDefault("all"),});Gestión de Estado de Formularios (React Hook Form)
React Hook Form gestiona el estado de formularios, validación y envío.
Patrón Básico
import { useForm } from "react-hook-form";import { zodResolver } from "@hookform/resolvers/zod";import { createOrderSchema } from "./schema";
const useCreateOrderForm = () => { const form = useForm({ resolver: zodResolver(createOrderSchema), defaultValues: { title: "", description: "", priority: "medium", }, });
const { mutate: createOrder } = useCreateOrder();
const onSubmit = form.handleSubmit((data) => { createOrder(data); });
return { form, onSubmit };};Integración con Zod
import { z } from "zod";
export const createOrderSchema = z.object({ title: z.string().min(3, "El título debe tener al menos 3 caracteres"), description: z.string().min(10, "La descripción debe tener al menos 10 caracteres"), priority: z.enum(["low", "medium", "high"]), assetId: z.string().optional(),});
export type CreateOrderInput = z.infer<typeof createOrderSchema>;Componente de Formulario
import { Form, InputTextFormField, Button } from '@/components/ui';import { useCreateOrderForm } from './hooks/use-create-order-form';
const CreateOrderForm = () => { const { form, onSubmit } = useCreateOrderForm();
return ( <Form {...form}> <form onSubmit={onSubmit} className="space-y-4"> <InputTextFormField name="title" control={form.control} label="Título de la Orden" placeholder="Ingrese título de la orden" />
<InputTextFormField name="description" control={form.control} label="Descripción" placeholder="Ingrese descripción" />
<Button type="submit" disabled={form.formState.isSubmitting}> {form.formState.isSubmitting ? 'Creando...' : 'Crear Orden'} </Button> </form> </Form> );};Árbol de Decisión de Gestión de Estado
flowchart TD
A[¿Necesita gestionar estado?] --> B{¿Qué tipo de datos?}
B -->|Datos API| C[Usar React Query]
B -->|Estado UI| D{¿Ámbito?}
D -->|Local del componente| E[Usar useState]
D -->|Compartido entre componentes| F{¿Necesita persistencia?}
F -->|Sí| G[Usar Zustand + localStorage]
F -->|No| H[Usar Zustand]
B -->|Datos de formulario| I[Usar React Hook Form]
B -->|Filtros/paginación URL| J[Usar nuqs]
Mejores Prácticas de Gestión de Estado
1. Estado del Servidor (React Query)
HACER:
- ✅ Usar para todos los datos de API
- ✅ Definir claves de consulta de manera consistente
- ✅ Invalidar consultas después de mutaciones
- ✅ Usar actualizaciones optimistas para mejor UX
- ✅ Configurar tiempo de frescura apropiadamente
- ✅ Manejar estados de carga y error
NO HACER:
- ❌ Almacenar datos del servidor en Zustand
- ❌ Gestionar manualmente el estado del servidor con useState
- ❌ Olvidar invalidar consultas después de mutaciones
- ❌ Usar invalidaciones excesivamente amplias
2. Estado del Cliente (Zustand)
HACER:
- ✅ Usar para estado UI compartido entre componentes
- ✅ Mantener stores pequeños y enfocados
- ✅ Usar selectores para prevenir re-renderizados innecesarios
- ✅ Persistir a localStorage cuando sea necesario
- ✅ Usar TypeScript para seguridad de tipos
NO HACER:
- ❌ Duplicar estado del servidor
- ❌ Crear un store global masivo
- ❌ Almacenar estado temporal de componente
- ❌ Usar para datos que deberían estar en la URL
3. Estado URL (nuqs)
HACER:
- ✅ Usar para filtros y paginación
- ✅ Hacer URLs compartibles
- ✅ Habilitar navegación del navegador
- ✅ Sincronizar con React Query
- ✅ Proporcionar valores predeterminados
NO HACER:
- ❌ Almacenar datos sensibles en URL
- ❌ Poner demasiados datos en URL
- ❌ Olvidar parsear tipos correctamente
4. Estado de Formularios (React Hook Form)
HACER:
- ✅ Usar para todos los formularios
- ✅ Integrar con Zod para validación
- ✅ Usar componentes controlados con moderación
- ✅ Aprovechar contexto de formulario para campos anidados
- ✅ Manejar errores de envío
NO HACER:
- ❌ Gestionar estado de formulario con useState
- ❌ Omitir validación
- ❌ Crear formularios no controlados para inputs complejos
5. Estado de Componente (useState)
HACER:
- ✅ Usar para estado verdaderamente local
- ✅ Estado de abrir/cerrar modales
- ✅ Estado UI temporal
- ✅ Estados de toggle
NO HACER:
- ❌ Elevar estado innecesariamente
- ❌ Almacenar datos del servidor
- ❌ Usar para estado compartido
Consideraciones de Rendimiento
Optimizaciones de React Query
- Caché: Reduce llamadas API con caché inteligente
- Tiempo de Frescura: Configura cuánto tiempo los datos están frescos
- Deduplicación: Previene solicitudes duplicadas
- Refrescamiento en Segundo Plano: Actualiza datos en segundo plano
Optimizaciones de Zustand
Los selectores previenen re-renderizados:
// Sin selector (re-renderiza con cualquier cambio de estado)const { user, notifications } = useStore();
// Con selector (solo re-renderiza cuando user cambia)const user = useStore((state) => state.user);Optimizaciones de Estado URL
- Actualizaciones Superficiales: Solo actualiza parámetros cambiados
- Debouncing: Puede hacer debounce de actualizaciones URL para búsqueda
Optimizaciones de Estado de Formulario
- Inputs No Controlados: Mejor rendimiento para formularios grandes
- Registro a Nivel de Campo: Solo re-renderiza campos cambiados
- Observar Campos Específicos: Evitar observar formulario completo
Depuración
Herramientas de Desarrollo de React Query
Abrir DevTools en desarrollo:
- Ver caché de consultas
- Inspeccionar datos de consulta
- Monitorear estados de carga
- Activar refrescamientos manuales
- Ver mutaciones
Herramientas de Desarrollo de Zustand
Puede agregarse con middleware:
import { devtools } from "zustand/middleware";
const useStore = create<State>()( devtools( (set) => ({ // definición del store }), { name: "MyStore" } ));Herramientas de Desarrollo del Navegador
- Estado URL: Visible en la barra de direcciones
- localStorage: Pestaña de Aplicación en DevTools
- Pestaña Network: Monitorear solicitudes API
- React DevTools: Estado y props de componentes
Consejos de Depuración
React Query:
// Registrar datos de consultaconst { data } = useOrders(filters);console.log("Orders:", data);
// Registrar estado de consultaconst { data, isLoading, error, isFetching } = useOrders(filters);console.log({ isLoading, error, isFetching });Zustand:
// Registrar estado del storeconst store = useStore();console.log("Store state:", store);
// Suscribirse a cambiosuseStore.subscribe((state) => console.log("State changed:", state));Referencias:
algesta-dashboard-react/src/lib/react-query.tsalgesta-dashboard-react/src/store/use-user-store.tsalgesta-dashboard-react/src/Funcionalidades/orders/hooks/use-order-filter-query-state.tsalgesta-dashboard-react/src/Funcionalidades/orders/api/get-orders.ts- Todos los archivos de store en
src/store/ - Todos los archivos de hooks en
src/Funcionalidades/*/hooks/