Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { createContext, use, useState, useCallback, type ReactNode } from "react";
// Divide context: separa estado de dispatch
const CountStateContext = createContext<number>(0);
const CountDispatchContext = createContext<{
increment: () => void;
decrement: () => void;
} | null>(null);
function CountProvider({ children }: { children: ReactNode }) {
const [count, setCount] = useState(0);
const dispatch = useMemo(() => ({
increment: () => setCount((c) => c + 1),
decrement: () => setCount((c) => c - 1),
}), []);
return (
<CountStateContext value={count}>
<CountDispatchContext value={dispatch}>
{children}
</CountDispatchContext>
</CountStateContext>
);
}Cuándo usarlo: Cuando tienes context que causa re-renderizados en componentes que solo necesitan parte del valor de context. Divide estado de acciones, limita context a subárboles, y usa selectores para leer solo lo que necesitas.
import {
createContext,
use,
useState,
useMemo,
useCallback,
memo,
type ReactNode,
} from "react";
// --- Divide context: estado de Tema vs acciones ---
interface ThemeState {
mode: "light" | "dark";
accentColor: string;
fontSize: number;
}
interface ThemeActions {
toggleMode: () => void;
setAccentColor: (color: string) => void;
setFontSize: (size: number) => void;
}
const ThemeStateContext = createContext<ThemeState>({
mode: "light",
accentColor: "#3b82f6",
fontSize: 16,
});
const ThemeActionsContext = createContext<ThemeActions | null>(null);
function ThemeProvider({ children }: { children: ReactNode }) {
const [state, setState] = useState<ThemeState>({
mode: "light",
accentColor: "#3b82f6",
fontSize: 16,
});
// Objeto de acciones estable - nunca causa re-renderizados en consumidores solo de acciones
const actions = useMemo<ThemeActions>(
() => ({
toggleMode: () =>
setState((s) => ({
...s,
mode: s.mode === "light" ? "dark" : "light",
})),
setAccentColor: (color) =>
setState((s) => ({ ...s, accentColor: color })),
setFontSize: (size) =>
setState((s) => ({ ...s, fontSize: size })),
}),
[]
);
return (
<ThemeStateContext value={state}>
<ThemeActionsContext value={actions}>
{children}
</ThemeActionsContext>
</ThemeStateContext>
);
}
// Hooks personalizados con comprobaciones de seguridad
function useThemeState() {
return use(ThemeStateContext);
}
function useThemeActions() {
const actions = use(ThemeActionsContext);
if (!actions) throw new Error("useThemeActions debe estar dentro de ThemeProvider");
return actions;
}
// --- Componentes demostrando consumo selectivo ---
// Solo re-renderiza cuando cambia el estado del tema
const ThemeIndicator = memo(function ThemeIndicator() {
const { mode, accentColor } = useThemeState();
console.log("ThemeIndicator renderizado");
return (
<div className="flex items-center gap-2">
<div
className="w-4 h-4 rounded-full"
style={{ backgroundColor: accentColor }}
/>
<span>{mode} mode</span>
</div>
);
});
// Solo re-renderiza cuando cambia el contexto de acciones (nunca, porque está memoizado)
const ThemeToggleButton = memo(function ThemeToggleButton() {
const { toggleMode } = useThemeActions();
console.log("ThemeToggleButton renderizado");
return (
<button onClick={toggleMode} className="px-3 py-1 border rounded">
Toggle Theme
</button>
);
});
// --- Patrón de context limitado en alcance ---
interface NotificationContextValue {
notifications: Notification[];
add: (message: string) => void;
dismiss: (id: string) => void;
}
const NotificationContext = createContext<NotificationContextValue | null>(null);
function useNotifications() {
const ctx = use(NotificationContext);
if (!ctx) throw new Error("useNotifications debe estar dentro de NotificationProvider");
return ctx;
}
interface Notification {
id: string;
message: string;
}
function NotificationProvider({ children }: { children: ReactNode }) {
const [notifications, setNotifications] = useState<Notification[]>([]);
const add = useCallback((message: string) => {
const id = crypto.randomUUID();
setNotifications((prev) => [...prev, { id, message }]);
setTimeout(() => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
}, 5000);
}, []);
const dismiss = useCallback((id: string) => {
setNotifications((prev) => prev.filter((n) => n.id !== id));
}, []);
const value = useMemo(
() => ({ notifications, add, dismiss }),
[notifications, add, dismiss]
);
return (
<NotificationContext value={value}>
{children}
</NotificationContext>
);
}
// --- Diseño de aplicación completa ---
function App() {
return (
<ThemeProvider>
<NotificationProvider>
<header className="flex justify-between p-4 border-b">
<ThemeIndicator />
<ThemeToggleButton />
</header>
<main className="p-6">
<ContentArea />
</main>
</NotificationProvider>
</ThemeProvider>
);
}Lo que esto demuestra:
ThemeStateContext y ThemeActionsContext están separadosuseMemo - consumidores solo de acciones nunca re-renderizadosmemo en componentes hoja para evitar re-renderizados de renders padresuseMemo en el objeto de acciones asegura que su referencia nunca cambie, haciendo el context de acciones estable.useMemo en el objeto de valor es solo útil si tienes múltiples campos de estado y quieres evitar re-renderizados cuando cambian campos no relacionados - pero como el objeto se recrea cuando cualquier campo cambia, es más efectivo con contexts divididos por dominio.use() puede leer context condicionalmente (dentro de sentencias if), a diferencia de useContext.| Patrón | Qué resuelve |
|---|---|
| Divide estado/acciones | Consumidores solo de acciones (botones, formularios) no re-renderizados en cambios de estado |
| Objeto de valor memoizado | Previene re-renderizados cuando la referencia de objeto cambiaría pero el contenido es el mismo |
| Proveedor limitado en alcance | Context solo disponible para subárbol que lo necesita |
| Hook personalizado con error | Detecta errores de proveedor faltante en tiempo de desarrollo |
| Valor predeterminado en createContext | Permite usar context sin proveedor (útil para valores predeterminados de tema) |
Patrón de selector con store externo - suscribirse solo a la pieza que necesitas:
import { useSyncExternalStore } from "react";
// Usa useSyncExternalStore para lecturas basadas en selectores
function useStoreSelector<T, S>(store: Store<T>, selector: (state: T) => S): S {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getSnapshot()),
() => selector(store.getServerSnapshot())
);
}
// Solo re-renderiza cuando `user.name` cambia
function UserName() {
const name = useStoreSelector(appStore, (s) => s.user.name);
return <span>{name}</span>;
}Context con reducer - para transiciones de estado complejas:
const TodoDispatchContext = createContext<React.Dispatch<TodoAction> | null>(null);
const TodoStateContext = createContext<TodoState>({ items: [] });
function TodoProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(todoReducer, { items: [] });
return (
<TodoStateContext value={state}>
<TodoDispatchContext value={dispatch}>
{children}
</TodoDispatchContext>
</TodoStateContext>
);
}createContext<T>(defaultValue) cuando el context puede funcionar sin un proveedor.createContext<T | null>(null) cuando un proveedor es requerido, y comprueba nulo en el hook personalizado.ThemeActions no ThemeActions | null) después de la comprobación de nulo.Context único con estado y acciones mixtos - Cada cambio de estado re-renderiza cada consumidor, incluso aquellos que solo llaman acciones. Solución: Divide en contextos de estado y acciones separados.
Valor de context inestable - Crear un objeto literal nuevo en el render del proveedor (value={{ a, b }}) causa que cada consumidor re-renderice. Solución: Usa useMemo para estabilizar la referencia de valor.
Proveedor muy alto en el árbol - Colocar un proveedor que cambia frecuentemente en la raíz de la aplicación re-renderiza el árbol completo de consumidores. Solución: Limita proveedores al subárbol más pequeño que los necesita.
Context dividido excesivamente - Crear docenas de contexts minúsculos añade complejidad y anidamiento de proveedores. Solución: Divide por frecuencia de actualización (cosas que cambian juntas deben vivir juntas). Usa Zustand o Jotai para suscripciones detalladas.
Valores de context predeterminados ocultando errores - Un valor predeterminado significativo significa que context funciona sin un proveedor, lo que puede enmascarar un proveedor faltante. Solución: Usa valor predeterminado null + hook personalizado con lanzamiento para proveedores requeridos.
| Enfoque | Compensación |
|---|---|
| Divide context | Cero dependencias; esfuerzo de división manual |
| Zustand | Selectores automáticos, sin proveedores; dependencia extra |
| Jotai | Estado atómico, re-renderizados detallados; modelo mental diferente |
| Redux + useSelector | Ecosistema maduro, depuración de viaje en el tiempo; boilerplate |
useSyncExternalStore | Funciona con cualquier store externo; API de nivel más bajo |
| Signals (futuro) | Reactividad detallada; aún no en React |
useMemo(() => actions, []), su referencia nunca cambia.const actions = useMemo<ThemeActions>(
() => ({
toggleMode: () => setState((s) => ({ ...s, mode: s.mode === "light" ? "dark" : "light" })),
setAccentColor: (color) => setState((s) => ({ ...s, accentColor: color })),
}),
[]
);use() puede ser llamado condicionalmente (dentro de sentencias if), a diferencia de useContext().use() también funciona con promesas para recuperación de datos basada en Suspense.useContext() todavía funciona en React 19 pero use() es la alternativa más flexible.a y b no han cambiado.useMemo o divide en contextos separados.createContext(defaultValue), componentes funcionan sin un proveedor.createContext<T | null>(null) y lanza en el hook personalizado cuando context es nulo.const MyContext = createContext<MyState | null>(null);
function useMyContext(): MyState {
const ctx = use(MyContext);
if (!ctx) throw new Error("useMyContext debe estar dentro de MyProvider");
return ctx;
}| null predeterminado y comprueba nulo en el hook personalizado.MyState (no nulo) después de la comprobación.useSyncExternalStore se suscribe a un store externo y lee una porción seleccionada del estado.const TodoStateContext = createContext<TodoState>({ items: [] });
const TodoDispatchContext = createContext<React.Dispatch<TodoAction> | null>(null);
function TodoProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(todoReducer, { items: [] });
return (
<TodoStateContext value={state}>
<TodoDispatchContext value={dispatch}>{children}</TodoDispatchContext>
</TodoStateContext>
);
}dispatch es estable (React lo garantiza), así que consumidores solo de dispatch nunca re-renderizados.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥