Context frente a Zustand
Receta
Decide cuándo usar React Context y cuándo usar Zustand. Context funciona para state de baja frecuencia, como temas. Zustand destaca para state de alta frecuencia con muchos consumidores que necesita re-renderizado selectivo.
// Context: Bueno para state que cambia raramente, con alcance del provider
const ThemeContext = createContext<"light" | "dark">("light");
// Zustand: Bueno para state global que cambia con frecuencia y con muchos consumidores
const useCartStore = create<CartStore>((set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
}));Ejemplo funcional
// ANTES: state basado en Context (provoca re-renderizados en todos los consumidores)
// context/app-context.tsx
"use client";
import { createContext, useContext, useState, useCallback, ReactNode } from "react";
interface AppState {
user: { name: string } | null;
theme: "light" | "dark";
notifications: string[];
sidebarOpen: boolean;
}
interface AppContextValue extends AppState {
setUser: (user: AppState["user"]) => void;
toggleTheme: () => void;
addNotification: (msg: string) => void;
toggleSidebar: () => void;
}
const AppContext = createContext<AppContextValue | null>(null);
export function AppProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<AppState["user"]>(null);
const [theme, setTheme] = useState<AppState["theme"]>("light");
const [notifications, setNotifications] = useState<string[]>([]);
const [sidebarOpen, setSidebarOpen] = useState(true);
const toggleTheme = useCallback(() => {
setTheme((t) => (t === "light" ? "dark" : "light"));
}, []);
const addNotification = useCallback((msg: string) => {
setNotifications((n) => [...n, msg]);
}, []);
const toggleSidebar = useCallback(() => {
setSidebarOpen((s) => !s);
}, []);
// Problema: objeto nuevo en cada renderizado -> todos los consumidores se re-renderizan
return (
<AppContext.Provider
value={{
user, theme, notifications, sidebarOpen,
setUser, toggleTheme, addNotification, toggleSidebar,
}}
>
{children}
</AppContext.Provider>
);
}
export const useApp = () => {
const ctx = useContext(AppContext);
if (!ctx) throw new Error("useApp debe estar dentro de AppProvider");
return ctx;
};// DESPUÉS: store de Zustand (re-renderizados selectivos, sin necesidad de provider)
// stores/app-store.ts
import { create } from "zustand";
interface AppStore {
user: { name: string } | null;
theme: "light" | "dark";
notifications: string[];
sidebarOpen: boolean;
setUser: (user: AppStore["user"]) => void;
toggleTheme: () => void;
addNotification: (msg: string) => void;
toggleSidebar: () => void;
}
export const useAppStore = create<AppStore>((set) => ({
user: null,
theme: "light",
notifications: [],
sidebarOpen: true,
setUser: (user) => set({ user }),
toggleTheme: () => set((s) => ({ theme: s.theme === "light" ? "dark" : "light" })),
addNotification: (msg) => set((s) => ({ notifications: [...s.notifications, msg] })),
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));// Migración de componentes
// ANTES:
function Header() {
const { user, theme, toggleTheme } = useApp();
// Se re-renderiza también cuando cambian notifications o sidebarOpen
return <header>...</header>;
}
// DESPUÉS:
function Header() {
const user = useAppStore((s) => s.user);
const theme = useAppStore((s) => s.theme);
const toggleTheme = useAppStore((s) => s.toggleTheme);
// Solo se re-renderiza cuando user o theme cambian realmente
return <header>...</header>;
}Inmersión profunda
Cómo funciona
- React Context re-renderiza a todos los consumidores cada vez que cambia el valor del provider. Aunque un consumidor solo use
theme, se re-renderiza cuando cambiannotifications. - Zustand usa suscripciones internas con selectores. Cada componente se suscribe a slices específicos del state y solo se re-renderiza cuando cambia ese slice.
- Context requiere un componente Provider en el árbol. Los stores de Zustand son singletons a nivel de módulo accesibles desde cualquier lugar.
- Context limita de forma natural el state a un subárbol. Zustand es global por defecto, pero puede limitarse usando
createStore+useStorecon un Context. - Las actualizaciones de state en Zustand son síncronas y se agrupan con React 18+. Las actualizaciones de state en Context pasan por el mecanismo setState de React.
Variaciones
Cuándo Context sigue siendo la opción correcta:
// Tema, locale o estado de autenticación que cambia raramente
// y se limita de forma natural a un subárbol
const ThemeContext = createContext<"light" | "dark">("light");
// Feature flags por segmento de ruta
const FeatureFlagContext = createContext<Record<string, boolean>>({});
// State del formulario limitado a un solo formulario
const FormContext = createContext<FormState | null>(null);Zustand con Context para limitar el alcance en SSR:
import { createStore, useStore } from "zustand";
import { createContext, useContext, useRef } from "react";
type CounterStore = ReturnType<typeof createCounterStore>;
const createCounterStore = (initialCount = 0) =>
createStore<{ count: number; increment: () => void }>((set) => ({
count: initialCount,
increment: () => set((s) => ({ count: s.count + 1 })),
}));
const CounterContext = createContext<CounterStore | null>(null);
export function CounterProvider({
children,
initialCount,
}: {
children: React.ReactNode;
initialCount?: number;
}) {
const storeRef = useRef<CounterStore>();
if (!storeRef.current) {
storeRef.current = createCounterStore(initialCount);
}
return (
<CounterContext.Provider value={storeRef.current}>
{children}
</CounterContext.Provider>
);
}
export function useCounterStore<T>(selector: (s: { count: number; increment: () => void }) => T) {
const store = useContext(CounterContext);
if (!store) throw new Error("Falta CounterProvider");
return useStore(store, selector);
}Notas de TypeScript
- Los tipos de Context requieren tipos separados para valor y acciones, o una interfaz combinada.
- Los tipos de Zustand son más simples porque el state y las acciones viven en la misma interfaz.
- Ambos enfoques funcionan con TypeScript. El patrón de selector de Zustand ofrece un mejor estrechamiento de tipos.
Errores comunes
- Context no es "lento". Es apropiado para state de baja frecuencia. El problema de rendimiento solo aparece con muchos consumidores y actualizaciones frecuentes.
- Dividir Context en varios providers (uno para el state y otro para las acciones) puede reducir re-renderizados innecesarios, pero añade complejidad.
- Los stores de Zustand son singletons. En SSR, todas las solicitudes comparten el mismo store a menos que uses el patrón Context + createStore.
- Migrar de Context a Zustand requiere eliminar el wrapper del Provider y cambiar las llamadas a
useContextpor llamadas auseStore(selector). Hazlo de forma incremental. useMemoen el valor de Context ayuda, pero no resuelve el problema fundamental de re-renderizado cuando cambia cualquier valor.- Zustand no se integra de forma nativa con React DevTools (usa Redux DevTools mediante el middleware devtools). El state de Context aparece en React DevTools.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| React Context | Integrado, sin dependencias, con alcance en el árbol | Re-renderiza a todos los consumidores ante cualquier cambio |
| Zustand | Re-renderizados selectivos, sin provider, API simple | Singleton global, consideraciones de SSR |
| Jotai | Modelo atómico, provider opcional | Modelo mental distinto |
| Redux Toolkit | Maduro, excelentes DevTools | Boilerplate, curva de aprendizaje pronunciada |
| use(Context) + useMemo | Reduce algunos re-renderizados | No resuelve el problema principal |
FAQs
¿Por qué React Context re-renderiza a todos los consumidores cuando cambia cualquier valor?
- Context proporciona un único objeto de valor a todos los consumidores.
- Cuando cambia cualquier propiedad de ese objeto, React re-renderiza cada componente que llama a
useContextsobre él. - No hay un mecanismo de selector integrado para suscribirse a campos específicos.
¿Cómo evita Zustand el problema de re-renderizado que tiene Context?
- Zustand usa suscripciones internas con funciones selector.
- Cada componente se suscribe a un slice específico del state y solo se re-renderiza cuando cambia ese slice.
- Esto es fundamentalmente distinto del enfoque de Context de "todo o nada".
¿Cuándo sigue siendo React Context la opción correcta frente a Zustand?
- Para state de baja frecuencia que cambia raramente: tema, locale, estado de autenticación.
- Cuando el state se limita de forma natural a un subárbol (p. ej., state del formulario dentro de un formulario).
- Cuando quieres cero dependencias externas.
¿Zustand requiere un componente Provider?
- No. Los stores de Zustand son singletons a nivel de módulo accesibles desde cualquier lugar.
- Excepción: en SSR/Next.js, usa Context +
createStorepara limitar los stores por solicitud.
¿Cómo migras un componente de Context a Zustand?
// Antes (Context):
const { user, theme } = useApp();
// Después (Zustand):
const user = useAppStore((s) => s.user);
const theme = useAppStore((s) => s.theme);- Elimina el wrapper del Provider y cambia las llamadas a
useContextporuseStore(selector).
Trampa: ¿React Context es realmente lento?
- Context en sí no es lento. El problema de rendimiento solo aparece con muchos consumidores y actualizaciones frecuentes.
- Para state que cambia raramente (como el tema), Context funciona perfectamente.
Trampa: ¿Envolver el valor de Context en useMemo resuelve el problema de re-renderizado?
useMemoreduce los re-renderizados causados por el re-renderizado del provider.- Pero no resuelve el problema principal: cualquier cambio en cualquier propiedad del valor re-renderiza a todos los consumidores.
¿Puedes dividir Context en varios providers para reducir los re-renderizados?
- Sí. Un provider para el state y otro para las acciones reduce algunos re-renderizados innecesarios.
- Pero esto añade complejidad y sigue sin ofrecer suscripciones basadas en selectores.
¿Cómo limitas un store de Zustand a un subárbol (como Context hace de forma natural)?
const StoreContext = createContext<MyStore | null>(null);
function Provider({ children }) {
const storeRef = useRef(createStore<State>((set) => ({ ... })));
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
);
}- Usa
createStore(vanilla) + Context +useStorepara instancias por subárbol.
¿En qué se diferencian los tipos de TypeScript entre los enfoques de Context y Zustand?
- Context requiere tipos separados para valor y acciones, además de una comprobación de
nullenuseContext. - Zustand combina state y acciones en una interfaz con inferencia automática de tipos del selector.
- El patrón de selector de Zustand ofrece un mejor estrechamiento de tipos.
¿Zustand se integra con React DevTools?
- No directamente. Los stores de Zustand no aparecen en React DevTools.
- Usa el middleware
devtoolspara conectarte a Redux DevTools en su lugar. - El state de Context sí aparece en React DevTools por defecto.