//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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] })),
}));// 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>;
}theme, se re-renderiza cuando cambian notifications.createStore + useStore con un Context.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);
}useContext por llamadas a useStore(selector). Hazlo de forma incremental.useMemo en el valor de Context ayuda, pero no resuelve el problema fundamental de re-renderizado cuando cambia cualquier valor.| 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 |
useContext sobre él.createStore para limitar los stores por solicitud.// Antes (Context):
const { user, theme } = useApp();
// Después (Zustand):
const user = useAppStore((s) => s.user);
const theme = useAppStore((s) => s.theme);useContext por useStore(selector).useMemo reduce los re-renderizados causados por el re-renderizado del provider.const StoreContext = createContext<MyStore | null>(null);
function Provider({ children }) {
const storeRef = useRef(createStore<State>((set) => ({ ... })));
return (
<StoreContext.Provider value={storeRef.current}>
{children}
</StoreContext.Provider>
);
}createStore (vanilla) + Context + useStore para instancias por subárbol.null en useContext.devtools para conectarte a Redux DevTools en su lugar.Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥