← Volver al listado de tecnologías

Frontend React con React Query

Por: SiempreListo
cqrsreactreact-querytypescriptfrontend

Capítulo 21: Frontend React con React Query

React Query (ahora TanStack Query) es una librería para gestionar estado del servidor en aplicaciones React. Su modelo de queries y mutations se alinea perfectamente con CQRS, proporcionando caché automático, revalidación y sincronización.

Por qué React Query con CQRS

React Query separa claramente:

Además ofrece:

Configuración Inicial

QueryClient es el núcleo de React Query. Gestiona el caché, las queries activas, y la configuración global.

staleTime define cuánto tiempo los datos se consideran “frescos”. Durante este tiempo, React Query no refetch automáticamente.

retry indica cuántas veces reintentar una query/mutation fallida antes de mostrar error.

// src/lib/query-client.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,
      retry: 2
    },
    mutations: {
      retry: 1
    }
  }
});

QueryClientProvider hace disponible el QueryClient en todo el árbol de componentes React.

// src/App.tsx
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './lib/query-client';

export function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <OrdersPage />
    </QueryClientProvider>
  );
}

API Client

El API Client encapsula las llamadas HTTP al backend. Cada función mapea a un endpoint del servidor.

fetch es la API nativa del navegador para hacer requests HTTP. import.meta.env accede a variables de entorno en Vite.

// src/api/orders.ts
const API_URL = import.meta.env.VITE_API_URL;

export const ordersApi = {
  getOrder: async (orderId: string): Promise<Order> => {
    const res = await fetch(`${API_URL}/orders/${orderId}`);
    if (!res.ok) throw new Error('Order not found');
    return res.json();
  },

  listOrders: async (customerId: string, page = 0): Promise<OrderList> => {
    const res = await fetch(`${API_URL}/orders?customerId=${customerId}&page=${page}`);
    return res.json();
  },

  createOrder: async (customerId: string): Promise<{ orderId: string }> => {
    const res = await fetch(`${API_URL}/orders`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ customerId })
    });
    return res.json();
  },

  addItem: async (orderId: string, item: ItemInput): Promise<void> => {
    await fetch(`${API_URL}/orders/${orderId}/items`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(item)
    });
  }
};

Hooks de Queries

useQuery es el hook principal para obtener datos. Maneja automáticamente loading, error, caché y revalidación.

queryKey identifica únicamente la query en el caché. Si los parámetros cambian, se hace una nueva query.

enabled controla cuándo ejecutar la query. !!orderId previene queries con ID vacío.

// src/hooks/useOrder.ts
import { useQuery } from '@tanstack/react-query';
import { ordersApi } from '../api/orders';

export const useOrder = (orderId: string) => {
  return useQuery({
    queryKey: ['order', orderId],
    queryFn: () => ordersApi.getOrder(orderId),
    enabled: !!orderId
  });
};

export const useOrders = (customerId: string, page = 0) => {
  return useQuery({
    queryKey: ['orders', customerId, page],
    queryFn: () => ordersApi.listOrders(customerId, page),
    enabled: !!customerId
  });
};

Hooks de Mutations (Comandos)

useMutation es el hook para operaciones de escritura. Equivale a los comandos de CQRS.

useQueryClient obtiene el cliente para manipular el caché programáticamente.

invalidateQueries marca queries como stale, forzando un refetch. Esto sincroniza el Read Side después de un comando.

onSuccess se ejecuta después de una mutation exitosa, ideal para invalidar caché relacionado.

// src/hooks/useOrderCommands.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ordersApi } from '../api/orders';

export const useCreateOrder = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (customerId: string) => ordersApi.createOrder(customerId),
    onSuccess: (_, customerId) => {
      queryClient.invalidateQueries({ queryKey: ['orders', customerId] });
    }
  });
};

export const useAddItem = (orderId: string) => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (item: ItemInput) => ordersApi.addItem(orderId, item),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['order', orderId] });
    }
  });
};

Componente de Lista de Pedidos

Los hooks de React Query retornan objetos con isLoading, error, y data. El componente renderiza diferente UI según el estado.

// src/components/OrderList.tsx
import { useOrders } from '../hooks/useOrder';

interface Props {
  customerId: string;
}

export function OrderList({ customerId }: Props) {
  const { data, isLoading, error } = useOrders(customerId);

  if (isLoading) return <div>Cargando pedidos...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {data?.orders.map(order => (
        <li key={order.id}>
          <span>Pedido #{order.id.slice(0, 8)}</span>
          <span>Total: ${order.total}</span>
          <span>Estado: {order.status}</span>
        </li>
      ))}
    </ul>
  );
}

Formulario de Crear Pedido

mutate ejecuta la mutation. isPending indica si está en progreso (util para deshabilitar botones).

El callback onSuccess permite ejecutar código adicional, como notificar al padre con el nuevo ID.

// src/components/CreateOrderForm.tsx
import { useCreateOrder } from '../hooks/useOrderCommands';

interface Props {
  customerId: string;
  onSuccess: (orderId: string) => void;
}

export function CreateOrderForm({ customerId, onSuccess }: Props) {
  const { mutate, isPending } = useCreateOrder();

  const handleSubmit = () => {
    mutate(customerId, {
      onSuccess: (data) => onSuccess(data.orderId)
    });
  };

  return (
    <button onClick={handleSubmit} disabled={isPending}>
      {isPending ? 'Creando...' : 'Crear Pedido'}
    </button>
  );
}

Agregar Item con Optimistic Update

Optimistic Update actualiza la UI inmediatamente antes de que el servidor confirme, asumiendo que la operación será exitosa. Si falla, se revierte al estado anterior.

setQueryData modifica el caché directamente. Recibe el queryKey y una función que transforma los datos existentes.

onError se ejecuta si la mutation falla, permitiendo revertir los cambios optimistas.

// src/components/AddItemForm.tsx
import { useAddItem } from '../hooks/useOrderCommands';
import { useQueryClient } from '@tanstack/react-query';

export function AddItemForm({ orderId }: { orderId: string }) {
  const queryClient = useQueryClient();
  const { mutate, isPending } = useAddItem(orderId);

  const handleSubmit = (item: ItemInput) => {
    // Optimistic update
    queryClient.setQueryData(['order', orderId], (old: Order) => ({
      ...old,
      items: [...old.items, item],
      total: old.total + item.price * item.quantity
    }));

    mutate(item, {
      onError: () => {
        queryClient.invalidateQueries({ queryKey: ['order', orderId] });
      }
    });
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      const data = new FormData(e.currentTarget);
      handleSubmit({
        productId: data.get('productId') as string,
        name: data.get('name') as string,
        price: Number(data.get('price')),
        quantity: Number(data.get('quantity'))
      });
    }}>
      <input name="productId" placeholder="ID Producto" required />
      <input name="name" placeholder="Nombre" required />
      <input name="price" type="number" step="0.01" required />
      <input name="quantity" type="number" min="1" required />
      <button type="submit" disabled={isPending}>Agregar</button>
    </form>
  );
}

Resumen

React Query con CQRS:

Glosario

React Query (TanStack Query)

Definición: Librería para gestionar estado del servidor en React. Maneja fetching, caching, sincronización y actualizaciones de datos remotos.

Por qué es importante: Elimina la necesidad de escribir lógica manual para loading, error, caché. Proporciona un modelo mental claro para datos del servidor.

Ejemplo práctico: useQuery({queryKey: ['order', id], queryFn: fetchOrder}) maneja automáticamente loading, error, caché, y refetch cuando el componente se monta.


QueryClient

Definición: Instancia central de React Query que gestiona el caché de queries, coordina refetches, y mantiene la configuración global.

Por qué es importante: Es el “cerebro” de React Query. Permite configurar comportamiento global y manipular el caché programáticamente.

Ejemplo práctico: queryClient.invalidateQueries(['orders']) marca todas las queries de orders como stale, forzando refetch la próxima vez que se usen.


Query Key

Definición: Array que identifica únicamente una query en el caché. Puede incluir strings, números, objetos con parámetros.

Por qué es importante: React Query usa el key para determinar si usar datos cacheados o hacer un nuevo request. Keys bien diseñados optimizan el caché.

Ejemplo práctico: ['order', '123'] y ['order', '456'] son dos queries diferentes. Cambiar de ID automáticamente hace un nuevo fetch.


staleTime

Definición: Tiempo en milisegundos que los datos se consideran frescos. Durante este tiempo, React Query no refetch aunque el componente se remonte.

Por qué es importante: Balance entre frescura de datos y número de requests. Datos que cambian poco pueden tener staleTime alto.

Ejemplo práctico: staleTime: 5 * 60 * 1000 (5 minutos) significa que después de fetch, los datos se usan del caché por 5 minutos sin nuevo request.


useMutation

Definición: Hook de React Query para operaciones que modifican datos en el servidor (POST, PUT, DELETE).

Por qué es importante: Proporciona estado de la mutation (pending, error, success), callbacks, y se integra con invalidación de queries.

Ejemplo práctico: const {mutate, isPending} = useMutation({mutationFn: createOrder}) permite llamar mutate(data) y mostrar loading mientras procesa.


invalidateQueries

Definición: Método del QueryClient que marca queries como stale (obsoletas), forzando refetch la próxima vez que se necesiten.

Por qué es importante: Es el mecanismo principal para sincronizar el Read Side después de un comando. Garantiza que las queries reflejen cambios recientes.

Ejemplo práctico: Después de addItem, llamar invalidateQueries(['order', orderId]) asegura que la próxima lectura del pedido incluya el nuevo item.


Optimistic Update

Definición: Técnica donde la UI se actualiza inmediatamente antes de confirmar con el servidor, asumiendo éxito. Si falla, se revierte.

Por qué es importante: Mejora dramáticamente la percepción de velocidad. El usuario ve cambios instantáneos en lugar de esperar al servidor.

Ejemplo práctico: Al agregar un item, se muestra inmediatamente en la lista. Si el servidor rechaza, se vuelve al estado anterior y se muestra error.


setQueryData

Definición: Método del QueryClient para modificar datos en el caché directamente sin hacer un request al servidor.

Por qué es importante: Permite optimistic updates y actualizar el caché basándose en respuestas de mutations sin queries adicionales.

Ejemplo práctico: setQueryData(['order', id], old => ({...old, status: 'confirmed'})) actualiza el estado del pedido en el caché localmente.


enabled

Definición: Opción de useQuery que controla si la query debe ejecutarse. Si es false, no se hace el request.

Por qué es importante: Permite queries condicionales. Evita requests innecesarios cuando faltan parámetros o datos previos.

Ejemplo práctico: enabled: !!userId previene la query cuando userId es null/undefined, evitando errores y requests inútiles.


← Capítulo 20: GraphQL | Capítulo 22: Testing →