Patrones de Context — Divide, limita y optimiza React context para evitar re-renderizados innecesarios
Receta
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.
Ejemplo funcional
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:
- Divide context:
ThemeStateContextyThemeActionsContextestán separados - Acciones estables vía
useMemo— consumidores solo de acciones nunca re-renderizados memoen componentes hoja para evitar re-renderizados de renders padres- Context de notificación limitado en alcance con despido automático
- Hooks personalizados con mensajes de error útiles para proveedores faltantes
Análisis profundo
Cómo funciona
- React context re-renderiza cada consumidor cada vez que el valor de context cambia (por referencia).
- Dividir estado y acciones en contextos separados significa que consumidores solo de acciones (como botones) no re-renderizados cuando cambia el estado.
useMemoen el objeto de acciones asegura que su referencia nunca cambie, haciendo el context de acciones estable.- Para el context de estado,
useMemoen 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. - React 19
use()puede leer context condicionalmente (dentro de sentencias if), a diferencia deuseContext.
Parámetros y valores de retorno
| 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) |
Variaciones
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>
);
}Notas de TypeScript
- Proporciona un valor predeterminado sensato a
createContext<T>(defaultValue)cuando el context puede funcionar sin un proveedor. - Usa
createContext<T | null>(null)cuando un proveedor es requerido, y comprueba nulo en el hook personalizado. - Tipifica el retorno del hook personalizado como no nulo (
ThemeActionsnoThemeActions | null) después de la comprobación de nulo. - Exporta tipos de context para que consumidores puedan tipizar sus propias abstracciones.
Trampas
-
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: UsauseMemopara 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.
Alternativas
| 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 |
Preguntas frecuentes
¿Por qué poner estado y acciones en un context único causa problemas de rendimiento?
- Cada cambio de estado re-renderiza cada consumidor, incluso aquellos que solo llaman acciones (como botones).
- Consumidores que nunca leen estado aún re-renderizados porque la referencia del valor de context cambia.
- Solución: divide en contextos de estado y acciones separados.
¿Cómo ayuda dividir estado y acciones en contextos separados al rendimiento?
- Consumidores solo de acciones (botones, formularios) se suscriben solo al context de acciones.
- Dado que el objeto de acciones está memoizado con
useMemo(() => actions, []), su referencia nunca cambia. - Consumidores solo de acciones nunca re-renderizados cuando cambia el estado.
¿Qué logra useMemo en el objeto de acciones en un proveedor de context?
const actions = useMemo<ThemeActions>(
() => ({
toggleMode: () => setState((s) => ({ ...s, mode: s.mode === "light" ? "dark" : "light" })),
setAccentColor: (color) => setState((s) => ({ ...s, accentColor: color })),
}),
[]
);- Crea una referencia estable que nunca cambia en renders.
- Consumidores del context de acciones nunca re-renderizados porque la referencia permanece igual.
¿Cuándo debes limitar un proveedor de context a un subárbol en lugar de la raíz de la aplicación?
- Cuando solo una porción de la aplicación necesita el context (p. ej., un sistema de notificaciones para una página).
- Cuando el valor de context cambia frecuentemente y causaría re-renderizados en componentes no relacionados.
- Limitar el alcance reduce el radio de impacto de actualizaciones de context.
¿Cómo difiere use() en React 19 de useContext()?
use()puede ser llamado condicionalmente (dentro de sentencias if), a diferencia deuseContext().use()también funciona con promesas para recuperación de datos basada en Suspense.useContext()todavía funciona en React 19 perouse()es la alternativa más flexible.
Trampa: ¿Por qué value={{ a, b }} en un proveedor causa re-renderizados innecesarios?
- Crear un objeto literal nuevo en el render crea una referencia nueva cada render.
- React comprueba el valor de context por referencia, así que cada consumidor re-renderiza incluso si
aybno han cambiado. - Solución: memoiza el valor con
useMemoo divide en contextos separados.
Trampa: ¿Cómo puede un valor de context predeterminado ocultar un error de proveedor faltante?
- Si proporcionas un valor predeterminado significativo a
createContext(defaultValue), componentes funcionan sin un proveedor. - Esto puede enmascarar un proveedor faltante en el árbol, llevando a errores sutiles donde componentes usan valores predeterminados obsoletos.
- Solución: usa
createContext<T | null>(null)y lanza en el hook personalizado cuando context es nulo.
¿Cómo tipificas un context que requiere un proveedor en TypeScript?
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;
}- Usa
| nullpredeterminado y comprueba nulo en el hook personalizado. - El tipo de retorno del hook es
MyState(no nulo) después de la comprobación.
¿Qué es el patrón de selector con useSyncExternalStore y cuándo es útil?
useSyncExternalStorese suscribe a un store externo y lee una porción seleccionada del estado.- Solo el valor seleccionado provoca re-renderizados, dando suscripción detallada sin división de context.
- Útil cuando necesitas lecturas basadas en selectores sin adoptar una biblioteca de gestión de estado completa.
¿Cómo combinas context con useReducer para transiciones de estado complejas?
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>
);
}- Divide estado y dispatch en contextos separados.
dispatches estable (React lo garantiza), así que consumidores solo de dispatch nunca re-renderizados.
¿Cuándo debes usar Zustand o Jotai en lugar de React context?
- Cuando necesitas suscripciones detalladas (selecciona un único campo sin re-renderizar en cambios no relacionados).
- Cuando tienes muchos valores que cambian frecuentemente y requerirían división excesiva de context.
- Cuando quieres una API más simple sin anidamiento de proveedor.
Relacionado
- Performance — Técnicas de memoización y optimización de re-renderizado
- Compound Components — Context es la columna vertebral de componentes compuestos
- Composition — La composición reduce la necesidad de context en muchos casos