Saltearse al contenido

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

  1. Filosofía de Gestión de Estado
  2. React Query (Estado del Servidor)
  3. Zustand (Estado del Cliente)
  4. Gestión de Estado URL (nuqs)
  5. Gestión de Estado de Formularios (React Hook Form)
  6. Árbol de Decisión de Gestión de Estado
  7. Mejores Prácticas de Gestión de Estado
  8. Consideraciones de Rendimiento
  9. 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):

src/lib/react-query.ts
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:

src/features/orders/api/get-orders.ts
// Patrón de ejemplo (simplificado) - adaptar a tu funcionalidad específica
import { 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ónEjemploUso
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:

src/store/use-user-store.ts
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 localStorage
if (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:

features/orders/hooks/use-order-filter-query-state.ts
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=20

Parseadores

ParseadorTipoEjemplo
parseAsStringstring | null?name=john
parseAsIntegernumber | null?page=2
parseAsBooleanboolean | null?Activo=true
parseAsArrayOfarray | null?ids=1,2,3
parseAsStringEnumenum | 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 consulta
const { data } = useOrders(filters);
console.log("Orders:", data);
// Registrar estado de consulta
const { data, isLoading, error, isFetching } = useOrders(filters);
console.log({ isLoading, error, isFetching });

Zustand:

// Registrar estado del store
const store = useStore();
console.log("Store state:", store);
// Suscribirse a cambios
useStore.subscribe((state) => console.log("State changed:", state));

Referencias:

  • algesta-dashboard-react/src/lib/react-query.ts
  • algesta-dashboard-react/src/store/use-user-store.ts
  • algesta-dashboard-react/src/Funcionalidades/orders/hooks/use-order-filter-query-state.ts
  • algesta-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/