La gestión del estado se encuentra en el corazón de cada aplicación React no trivial. Hágalo bien y su código base escalará con gracia; Si lo hace mal, terminará con pesadillas que provocan perforaciones, una interfaz de usuario obsoleta y componentes que se vuelven a renderizar en el olvido. La buena noticia es que el ecosistema React ha madurado dramáticamente. Ahora tiene un amplio menú de herramientas diseñadas específicamente (Redux Toolkit, Zustand, Jotai, TanStack Query y el Context API integrado), cada una optimizada para una clase específica de problema.
Este artículo analiza cada solución en profundidad, explica las ventajas y desventajas con ejemplos concretos y le brinda un marco de decisión que puede aplicar inmediatamente a sus propios proyectos.
Comprender los diferentes tipos de Estado
Antes de recurrir a cualquier biblioteca, vale la pena ser preciso sobre qué tipo de estado estás gestionando realmente. La combinación de diferentes categorías de estados es la causa principal de la mayoría de las aplicaciones React con demasiada ingeniería.
- Estado de la interfaz de usuario local — si un menú desplegable está abierto, qué pestaña está activa, el valor actual de una entrada controlada. Este estado es propiedad de un único componente o de un pequeño subárbol.
- Estado de cliente compartido – datos que múltiples componentes potencialmente distantes necesitan leer o escribir: el usuario actualmente autenticado, preferencias de tema, un carrito de compras.
- Estado del servidor — datos que se originan en un servidor, son asincrónicos por naturaleza y necesitan invalidación de caché, recuperación en segundo plano y gestión del ciclo de vida de carga/error.
- Estado de la URL – filtros, paginación y términos de búsqueda que deberían sobrevivir a una actualización de la página y poder compartirse a través de un enlace.
- Estado del formulario – entradas controladas, errores de validación y estado de envío, que a menudo se manejan mejor con una biblioteca dedicada como React Hook Form.
Lo más impactante que se puede hacer es resistir la tentación de agrupar cada categoría de Estado en un almacén global. Cada categoría tiene diferentes requisitos de coherencia, diferentes duraciones y diferentes frecuencias de actualización.
Contexto de reacción: la opción incorporada
Context API se envía con React y no requiere dependencias adicionales. Funciona bien para estados que cambian con poca frecuencia y son consumidos por muchos componentes; los ejemplos clásicos son el tema, la configuración regional y el objeto de usuario autenticado.
// AuthContext.tsx
import { createContext, useContext, useState, ReactNode } from 'react';
interface AuthState {
user: User | null;
login: (credentials: Credentials) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthState | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (credentials: Credentials) => {
const user = await authService.login(credentials);
setUser(user);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used inside AuthProvider');
return ctx;
}
La trampa del desempeño contextual
El contexto tiene una característica de rendimiento bien conocida que hace tropezar a muchos desarrolladores: cada consumidor vuelve a renderizar cada vez que cambia la referencia del valor del contexto. Si almacena un objeto grande y que muta con frecuencia en un solo contexto, provocará renderizaciones innecesarias en todo el árbol.
Las estrategias de mitigación son:
- Dividir contextos por frecuencia de actualización. Mantenga el objeto de usuario en un contexto y las preferencias de la interfaz de usuario en otro.
- Memorice el objeto de valor con
useMemoentonces la referencia solo cambia cuando los datos realmente cambian. - Usar
React.memoen los componentes del consumidor para omitir las re-renderizaciones cuando los accesorios que les interesan no han cambiado.
La conclusión honesta es que Context es excelente para el estado global de baja frecuencia, pero no es una solución de gestión del estado de propósito general. Una vez que se encuentre escribiendo memorizaciones elaboradas para solucionar el comportamiento de renderizado de Context, es hora de buscar una biblioteca dedicada.
Kit de herramientas de Redux: la elección empresarial madura
Redux tiene fama de ser repetitivo, una reputación que se ganó en la era anterior a Toolkit. Redux Toolkit (RTK) elimina la ceremonia y al mismo tiempo preserva lo que hace que Redux sea poderoso: un árbol de estado único, predecible e inspeccionable con un estricto flujo de datos unidireccional.
Barcos RTK createSlice, que genera creadores y reductores de acciones a partir de una única definición, y utiliza Immer internamente para que puedas escribir mutaciones que parecen imperativas pero que en realidad se aplican de forma inmutable.
// features/cart/cartSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CartItem {
id: string;
name: string;
quantity: number;
price: number;
}
interface CartState {
items: CartItem[];
coupon: string | null;
}
const initialState: CartState = { items: [], coupon: null };
export const cartSlice = createSlice({
name: 'cart',
initialState,
reducers: {
addItem(state, action: PayloadAction<CartItem>) {
const existing = state.items.find(i => i.id === action.payload.id);
if (existing) {
existing.quantity += action.payload.quantity;
} else {
state.items.push(action.payload);
}
},
removeItem(state, action: PayloadAction<string>) {
state.items = state.items.filter(i => i.id !== action.payload);
},
applyCoupon(state, action: PayloadAction<string>) {
state.coupon = action.payload;
},
},
});
export const { addItem, removeItem, applyCoupon } = cartSlice.actions;
export default cartSlice.reducer;
Consulta RTK: estado del servidor dentro de Redux
RTK incluye un complemento llamado RTK Query que maneja el estado del servidor directamente dentro de la tienda Redux. Genera automáticamente enlaces de React a partir de definiciones de puntos finales y gestiona el almacenamiento en caché, la invalidación y las actualizaciones optimistas.
// services/productsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsApi = createApi({
reducerPath: 'productsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Product'],
endpoints: (builder) => ({
getProducts: builder.query<Product[], void>({
query: () => '/products',
providesTags: ['Product'],
}),
updateProduct: builder.mutation<Product, Partial<Product>>({
query: ({ id, ...patch }) => ({
url: `/products/${id}`,
method: 'PATCH',
body: patch,
}),
invalidatesTags: ['Product'],
}),
}),
});
export const { useGetProductsQuery, useUpdateProductMutation } = productsApi;
Redux Toolkit es la elección correcta cuando necesita depurar viajes en el tiempo a través de Redux DevTools, cuando varios equipos necesitan contribuir a un modelo de estado compartido con convenciones aplicadas o cuando tiene una lógica empresarial compleja entre sectores que se beneficia de middleware como redux-saga o redux-observable.
Zustand: Estado global ligero y sin ceremonias
Zustand adopta un enfoque radicalmente diferente. No hay proveedor, reductor ni tipo de acción. Usted define una tienda como un objeto JavaScript simple con estado y métodos, luego la consume con un solo enlace. Todo el API cabe en una sola pantalla.
// stores/useCartStore.ts
import { create } from 'zustand';
import { persist, devtools } from 'zustand/middleware';
interface CartStore {
items: CartItem[];
addItem: (item: CartItem) => void;
removeItem: (id: string) => void;
totalPrice: () => number;
clear: () => void;
}
export const useCartStore = create<CartStore>()(
devtools(
persist(
(set, get) => ({
items: [],
addItem: (item) =>
set((state) => {
const existing = state.items.find((i) => i.id === item.id);
if (existing) {
return {
items: state.items.map((i) =>
i.id === item.id
? { ...i, quantity: i.quantity + item.quantity }
: i
),
};
}
return { items: [...state.items, item] };
}),
removeItem: (id) =>
set((state) => ({ items: state.items.filter((i) => i.id !== id) })),
totalPrice: () =>
get().items.reduce((sum, i) => sum + i.price * i.quantity, 0),
clear: () => set({ items: [] }),
}),
{ name: 'cart-storage' }
)
)
);
Suscripciones granulares y rendimiento
Zustand resuelve el problema de renderizado de Context mediante suscripciones basadas en selectores. Un componente solo se vuelve a renderizar cuando la porción de estado que seleccionó ha cambiado.
// Only re-renders when items.length changes, not on price changes
const itemCount = useCartStore((state) => state.items.length);
// Only re-renders when the total changes
const total = useCartStore((state) => state.totalPrice());
El ecosistema de middleware de Zustand cubre la persistencia para localStorage, integración de DevTools, mutaciones de estilo Immer y sincronización de URL. Es la opción preferida para los equipos que desean capacidad de nivel Redux sin costo de configuración de nivel Redux.
Jotai: Estado atómico inspirado en el retroceso
Jotai modela el estado como un gráfico de átomos: pequeñas unidades de estado componibles que pueden derivarse unas de otras. Este modelo es particularmente poderoso para el estado dinámico: piense en una lista de nodos del editor donde cada nodo tiene su propio estado de selección/enfoque independiente, o una forma compleja donde la visibilidad del campo depende de otros valores de campo.
// atoms/filterAtoms.ts
import { atom, selector } from 'jotai';
export const searchTermAtom = atom('');
export const categoryAtom = atom<string | null>(null);
export const productsAtom = atom<Product[]>([]);
// Derived atom — recomputes only when its dependencies change
export const filteredProductsAtom = atom((get) => {
const products = get(productsAtom);
const term = get(searchTermAtom).toLowerCase();
const category = get(categoryAtom);
return products.filter((p) => {
const matchesTerm = p.name.toLowerCase().includes(term);
const matchesCategory = category === null || p.category === category;
return matchesTerm && matchesCategory;
});
});
// ProductList.tsx
import { useAtom, useAtomValue } from 'jotai';
function ProductList() {
const [searchTerm, setSearchTerm] = useAtom(searchTermAtom);
const filtered = useAtomValue(filteredProductsAtom);
return (
<>
<input value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} />
{filtered.map((p) => <ProductCard key={p.id} product={p} />)}
</>
);
}
El modelo de reactividad detallado de Jotai significa que cuando el término de búsqueda cambia, solo los componentes que realmente se suscriben filteredProductsAtom volver a renderizar. Componentes suscritos únicamente a categoryAtom están intactos. Esto hace que Jotai sea excelente para aplicaciones con gráficos de estado densos e interdependientes.
Consulta TanStack: el hogar adecuado para el estado del servidor
TanStack Query (anteriormente React Query) no es una biblioteca de administración de estado general; es una biblioteca de estado de servidor y maneja esa responsabilidad mejor que cualquier otra cosa en el ecosistema. Obtener, almacenar en caché, sincronizar y actualizar datos remotos en React sin TanStack Query generalmente significa administrar manualmente indicadores de carga, objetos de error e invalidación de caché ya sea en el estado local o en un almacén global. TanStack Query automatiza todo eso.
// hooks/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
export function useProducts(filters: ProductFilters) {
return useQuery({
queryKey: ['products', filters],
queryFn: () => api.getProducts(filters),
staleTime: 5 * 60 * 1000, // treat data as fresh for 5 minutes
gcTime: 10 * 60 * 1000, // keep unused data in cache for 10 minutes
});
}
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (update: ProductUpdate) => api.updateProduct(update),
// Optimistic update
onMutate: async (update) => {
await queryClient.cancelQueries({ queryKey: ['products'] });
const previous = queryClient.getQueryData(['products']);
queryClient.setQueryData(['products'], (old: Product[]) =>
old.map((p) => (p.id === update.id ? { ...p, ...update } : p))
);
return { previous };
},
onError: (_err, _update, context) => {
queryClient.setQueryData(['products'], context?.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
La matriz de claves de consulta es la primitiva de almacenamiento en caché de TanStack Query. Cualquier consulta que comparta la misma clave comparte la misma entrada de caché. Cuando invalida una clave, cada componente suscrito a esa clave se recupera automáticamente en segundo plano y se actualiza cuando llegan nuevos datos.
Precarga e hidratación en Next.js
En una aplicación Next.js, puede precargar consultas en el servidor y deshidratarlas en la carga útil HTML, eliminando por completo la carga de los controles giratorios para la representación de la página inicial.
// app/products/page.tsx (Next.js App Router)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query';
export default async function ProductsPage() {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: ['products', {}],
queryFn: () => api.getProducts({}),
});
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<ProductList />
</HydrationBoundary>
);
}
Combinando bibliotecas: una arquitectura práctica
Las aplicaciones reales casi siempre se benefician de la combinación de múltiples herramientas, cada una responsable de su propia categoría de estado. Una aplicación React mediana a grande bien estructurada podría verse así:
- Consulta TanStack posee todo el estado del servidor. Recupera, almacena en caché y sincroniza datos remotos. Nada más toca las respuestas de API.
- Zustand posee el estado global del cliente: el carrito de compras, la sesión del usuario autenticado, las preferencias de la interfaz de usuario y cualquier coordinación entre funciones que sea demasiado compleja para el contexto.
- Reaccionar contexto posee una configuración de baja rotación: el tema actual, la configuración regional y los indicadores de funciones.
- useState / useReducer propio estado del componente local: modal abierto/cerrado, valores de campo de formulario (o React Hook Form para formularios complejos), selección de pestañas.
- Parámetros de búsqueda de URL propio estado de navegación: filtros activos, orden de clasificación, número de página actual.
Esta separación de preocupaciones no es sólo estética. Significa que su caché de estado de servidor nunca se contamina con el estado de la interfaz de usuario, su tienda de clientes global nunca se llena con datos de respuesta que TanStack Query almacenaría en caché de manera más eficiente y su contexto nunca activa re-renderizaciones en toda la aplicación porque alguien almacenó un valor que cambia rápidamente en ella.
Patrones de rendimiento que vale la pena conocer
Selectores memorizados con reselección
Al derivar valores calculados de una tienda Redux, use Reselect para evitar volver a calcular en cada renderizado. Reexportaciones RTK createSelector desde Volver a seleccionar.
import { createSelector } from '@reduxjs/toolkit';
const selectItems = (state: RootState) => state.cart.items;
export const selectCartSummary = createSelector(selectItems, (items) => ({
count: items.reduce((n, i) => n + i.quantity, 0),
total: items.reduce((sum, i) => sum + i.price * i.quantity, 0),
}));
Dividir renderizados con useTransition
Reaccionar 18 useTransition le permite marcar una actualización de estado como no urgente para que el navegador pueda interrumpirla para manejar trabajos de mayor prioridad, como la entrada del usuario. Esto es particularmente útil cuando un cambio de estado desencadena una costosa repetición de renderizado.
const [isPending, startTransition] = useTransition();
const handleFilterChange = (value: string) => {
startTransition(() => {
setFilterValue(value); // expensive downstream re-render is deferrable
});
};
Evitar problemas de identidad de objetos
Una fuente común de renderizaciones fantasmas es la creación de nuevos objetos o matrices literales dentro del renderizado. useMemo y useCallback Estas son sus herramientas aquí, pero aplíquelas sólo cuando tenga evidencia de un problema de rendimiento: la memorización prematura agrega una sobrecarga cognitiva sin un beneficio garantizado.
Marco de decisión
Utilice el siguiente árbol de decisiones al elegir un enfoque de gestión de estado para una nueva característica:
- ¿Es el estado puramente local para un componente o un pequeño subárbol? Usar
useStateouseReducer. - ¿El estado representa datos obtenidos de un servidor? Usar Consulta TanStack.
- ¿Es el estado del cliente global lo que necesitan muchos componentes del árbol? Alargar la mano hacia Zustand a menos que ya estés en una base de código Redux.
- ¿El estado cambia raramente y tiene un propósito de configuración (tema, configuración regional)? Usar Reaccionar contexto.
- ¿Está el Estado altamente interconectado con muchos valores derivados? Considerar Jotai.
- ¿Necesita depuración de viajes en el tiempo, convenciones arquitectónicas estrictas o sagas asíncronas complejas? Usar Kit de herramientas Redux.
Antipatrones comunes que se deben evitar
- Almacenamiento de respuestas del servidor en Redux o Zustand. Deje que TanStack Query sea propietario de esos datos. Duplicarlo en una tienda global crea errores de sincronización.
- Colocando todo en una sola tienda masiva. Un estado no relacionado que pertenece al mismo segmento fuerza un acoplamiento innecesario y dificulta las pruebas.
- Uso de Context para actualizaciones de alta frecuencia. Cada cambio de valor de contexto vuelve a representar a todos los consumidores. Para valores que cambian con cada pulsación de tecla o cuadro de animación, Contexto es la herramienta equivocada.
- Derivación del estado dentro de los componentes. Los valores calculados que dependen del estado deben ser selectores memorizados o átomos derivados, no cálculos en línea que se ejecutan en cada renderizado.
- Ignorando los estados de carga y error. El estado del servidor es inherentemente asincrónico. Superficies de consulta TanStack
isLoading,isError, yerrorpara cada consulta; úsalos.
Conclusión
El panorama de la gestión estatal en React nunca ha sido más saludable. Ya no tendrás que elegir entre una estructura pesada y construir todo desde cero. Redux Toolkit ofrece previsibilidad de nivel empresarial sin el modelo estándar de su predecesor. Zustand aporta esa misma potencia con una fracción de la configuración. Jotai resuelve los problemas difíciles en gráficos de estado dinámicos y detallados. Y TanStack Query ha resuelto definitivamente el estado del servidor, si todavía está administrando respuestas API en useEffect y useState, te debes a ti mismo migrar.
La información más importante es categórica: identifique primero con qué tipo de estado está tratando y luego seleccione la herramienta optimizada para esa categoría. Resista el instinto de canalizar todo en un solo sistema. Un enfoque en capas (TanStack Query para el estado del servidor, Zustand para el estado global del cliente, Contexto para la configuración y estado local para todo lo demás) produce bases de código sobre las que es más fácil razonar, más fáciles de probar y mucho más fáciles de mantener a medida que crecen los equipos y los requisitos.