Saltar al contenido

Gestión de estado en 2026: qué usar y cuándo

Guía práctica para elegir la gestión de estado adecuada en React moderno — de useState a Zustand y React Query — y el razonamiento tras cada opción.

3 min de lectura
Árbol de decisiones de "¿nuevo estado?" a cuatro hogares: hooks de UI locales, una caché de servidor de TanStack Query, una tienda de cliente de Zustand y parámetros de URL.

La paradoja de la gestión de estado

El ecosistema nunca ha tenido tantas opciones de gestión de estado, y sin embargo la mayoría de las aplicaciones solo necesita un puñado de ellas. El error que veo con más frecuencia es recurrir a una librería de estado global antes de probar soluciones más simples.

Este es mi marco de decisión, perfeccionado tras trabajar en decenas de aplicaciones React.

La jerarquía del estado

Antes de elegir una herramienta, clasifica lo que estás gestionando:

CategoríaEjemplosMejor herramienta
Estado de UI localmodal abierto, campo de formulario, hoveruseState / useReducer
Estado de UI compartidopestaña seleccionada, barra lateral abiertaContext o Zustand
Caché del servidordatos de usuario, listado de productosTanStack Query / SWR
Estado en la URLfiltros, paginación, búsquedaParámetros de URL / nuqs
Preferencias persistentestema, idiomalocalStorage + context

La mayoría de las decisiones de "necesitamos Redux" en realidad son decisiones de "necesitamos TanStack Query".

Capa 1: estado local — empieza por aquí

Empieza siempre por el estado local. La gran mayoría del estado de UI pertenece a un único componente.

tstypescript
// ✅ This doesn't need to be global
function Modal({ onClose }: { onClose: () => void }) {
  const [step, setStep] = useState<"details" | "confirm" | "done">("details");
  const [formData, setFormData] = useState({ name: "", email: "" });
 
  return (
    <dialog>
      {step === "details" && (
        <DetailsForm data={formData} onChange={setFormData} onNext={() => setStep("confirm")} />
      )}
      {step === "confirm" && (
        <ConfirmStep data={formData} onBack={() => setStep("details")} onSubmit={handleSubmit} />
      )}
    </dialog>
  );
}

Para estado local complejo con múltiples subvalores, useReducer da como resultado un código más limpio que usar varias llamadas a useState.

Capa 2: estado del servidor — TanStack Query

Si estás guardando datos de una API en un store global, estás yendo a contracorriente. El estado del servidor tiene propiedades particulares: es asíncrono, puede quedar obsoleto y puede ser invalidado por otras operaciones. TanStack Query está diseñado justamente para esto.

tstypescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
 
function UserProfile({ userId }: { userId: string }) {
  const queryClient = useQueryClient();
 
  // Automatic loading, error, and staleness handling
  const { data: user, isLoading } = useQuery({
    queryKey: ["users", userId],
    queryFn: () => fetchUser(userId),
    staleTime: 5 * 60 * 1000, // Fresh for 5 minutes
  });
 
  const updateUser = useMutation({
    mutationFn: (data: Partial<User>) => patchUser(userId, data),
    onSuccess: () => {
      // Invalidate and refetch after mutation
      queryClient.invalidateQueries({ queryKey: ["users", userId] });
    },
  });
 
  if (isLoading) return <Skeleton />;
 
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      updateUser.mutate({ name: e.target.name.value });
    }}>
      <input name="name" defaultValue={user.name} />
      <button type="submit" disabled={updateUser.isPending}>
        {updateUser.isPending ? "Saving..." : "Save"}
      </button>
    </form>
  );
}

Esto sustituye al manejo manual del estado de carga, el estado de error, la lógica de caché, el refetching en segundo plano y las actualizaciones optimistas — todo eso que antes vivía en tu store global.

Capa 3: estado en la URL — infrautilizado y subestimado

Los filtros, la paginación, el orden y las búsquedas pertenecen a la URL. Es estado gratuito que:

  • Sobrevive a las recargas de página
  • Se puede compartir mediante un enlace
  • Funciona con los botones de avanzar/retroceder del navegador
  • Es indexable por los motores de búsqueda
tstypescript
import { useQueryStates } from "nuqs";
 
function ProductList() {
  const [params, setParams] = useQueryStates({
    page: parseAsInteger.withDefault(1),
    sort: parseAsString.withDefault("newest"),
    category: parseAsString.withDefault(""),
    minPrice: parseAsInteger.withDefault(0),
  });
 
  const { data } = useQuery({
    queryKey: ["products", params],
    queryFn: () => fetchProducts(params),
  });
 
  return (
    <div>
      <FilterBar filters={params} onChange={setParams} />
      <ProductGrid products={data?.items} />
      <Pagination
        page={params.page}
        total={data?.total}
        onChange={(page) => setParams({ page })}
      />
    </div>
  );
}

Capa 4: estado de cliente — Zustand cuando lo necesitas

Cuando tienes un estado que es:

  • Compartido entre muchos componentes distantes
  • No proviene del servidor
  • Difícil de colocar cerca de donde se usa

Zustand es la solución más limpia: mínimo boilerplate, sin necesidad de providers y con excelente soporte de TypeScript.

tstypescript
import { create } from "zustand";
import { persist } from "zustand/middleware";
 
interface CartStore {
  items: CartItem[];
  addItem: (product: Product, quantity: number) => void;
  removeItem: (productId: string) => void;
  clearCart: () => void;
  total: () => number;
}
 
export const useCartStore = create<CartStore>()(
  persist(
    (set, get) => ({
      items: [],
 
      addItem: (product, quantity) =>
        set((state) => {
          const existing = state.items.find((i) => i.productId === product.id);
          if (existing) {
            return {
              items: state.items.map((i) =>
                i.productId === product.id
                  ? { ...i, quantity: i.quantity + quantity }
                  : i,
              ),
            };
          }
          return {
            items: [
              ...state.items,
              { productId: product.id, product, quantity },
            ],
          };
        }),
 
      removeItem: (productId) =>
        set((state) => ({
          items: state.items.filter((i) => i.productId !== productId),
        })),
 
      clearCart: () => set({ items: [] }),
 
      total: () =>
        get().items.reduce(
          (sum, item) => sum + item.product.price * item.quantity,
          0,
        ),
    }),
    { name: "cart-storage" }, // Auto-persisted to localStorage
  ),
);

El árbol de decisión

New state to manage?
│
├─ Is it UI state for one component?
│  └─ YES → useState / useReducer
│
├─ Is it data from a server?
│  └─ YES → TanStack Query
│
├─ Should it be in the URL?
│  └─ YES → URL params (nuqs)
│
├─ Is it shared across many components?
│  └─ YES → Zustand
│
└─ Is it a user preference that should persist?
   └─ YES → localStorage + small Zustand slice

El mejor sistema de gestión de estado es el que casi ni notas. Si estás constantemente peleando con tu librería de estado, probablemente esté haciendo más trabajo del que le corresponde, trabajo que otras capas podrían resolver mejor.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX