Uno de los errores conceptuales más arraigados en el ecosistema React es concebir a React Context como un gestor de estado global integral (State Manager).
Desde la perspectiva del motor interno de React Fiber, React Context es única y exclusivamente un mecanismo de inyección de dependencias (Dependency Injection). Su cometido es transportar datos de configuración inmutables o de baja frecuencia (idiomas, temas de interfaz, tokens de telemetría, servicios DI) a lo largo de la jerarquía de componentes sin incurrir en Prop Drilling. Alojar estados dinámicos de alta frecuencia en un Context monolítico desata inevitablemente tormentas de re-renderizados en cascada que no pueden ser frenadas con React.memo.
Emplear un contexto monolítico para gobernar estado dinámico introduce tres problemas sistémicos de rendimiento:
useContext(MyContext) o el hook use(MyContext) de React 19, la envoltura React.memo queda completamente anulada: al alterarse la referencia value en el Provider, React Fiber fuerza la actualización del componente.{ user, cart, theme, notifications }, mutar una sola propiedad secundaria (como agregar un producto al carrito) provoca el re-renderizado forzado de la barra de navegación, el perfil, el conmutador de tema y las notificaciones.(state) => state.field, obligando a fragmentar proveedores (Context Splitting) o a construir un puente de selección mediante useSyncExternalStore.En el código fuente de React Fiber, las suscripciones a Context funcionan mediante los siguientes mecanismos:
useContext, React añade un descriptor a la lista dependencies del Fiber activo.value en el Provider cambia, React marca el nodo Fiber y programa su actualización.// Contexto monolítico ingenuo: re-renderiza a todos los suscriptores en cada cambio
<AppContext.Provider value={{ user, cart, theme, dispatch }}>
// Arquitectura robusta: Context Splitting
<CartStateContext.Provider value={cartState}>
<CartDispatchContext.Provider value={dispatch}>
{children}
</CartDispatchContext.Provider>
</CartStateContext.Provider>Inspeccione con React DevTools Profiler qué nodos se iluminan al cambiar el contexto. Si la frecuencia supera 5–10 veces por segundo, Context es la herramienta equivocada.
Inyecte los componentes consumidores en el Provider a través de {children}, evitando la regeneración de subárboles no suscritos.
Divida el contexto monolítico en dos: StateContext para datos reactivos y DispatchContext/ActionsContext para funciones de control.
Envuelva el valor del contexto en useMemo para conservar la identidad referencial de Object.is mientras los datos permanezcan estables.
Si los componentes precisan propiedades individuales de un estado complejo, implemente un puente selector vía createStoreContext.
Un componente Button está envuelto en React.memo(Button) y consume const dispatch = useContext(CartContext). ¿Por qué el botón se re-renderiza ante cualquier cambio en el carrito de compras a pesar de que sus propias props son invariables?
TabsList, TabsTrigger, TabsContent) mediante contextos locales ligeros.En el código fuente de React Fiber durante la fase beginWork, React.memo evalúa la igualdad de props (shallowEqual(prevProps, nextProps)). No obstante, con anterioridad Fiber analiza las dependencias de contexto del nodo:
// Pseudocódigo conceptual del runtime de React Fiber en beginWork:
if (hasContextChanged(workInProgress.dependencies)) {
// ¡Se marca forzosamente para ejecución!
// ¡La verificación de props de React.memo se descarta por completo!
return updateFunctionComponent(...);
}Si Object.is(prevContextValue, nextContextValue) === false, el componente se re-renderiza siempre.
Para blindar a los componentes frente a ejecuciones innecesarias, el estado monolítico se segrega en dos proveedores complementarios:
'use client';
import React, { createContext, useContext, useReducer, useMemo } from 'react';
export interface CartItem {
readonly id: string;
readonly title: string;
readonly price: number;
}
export interface CartState {
readonly items: CartItem[];
readonly total: number;
}
export type CartAction =
| { type: 'ADD'; item: CartItem }
| { type: 'REMOVE'; id: string }
| { type: 'CLEAR' };
const CartStateContext = createContext<CartState | undefined>(undefined);
const CartDispatchContext = createContext<React.Dispatch<CartAction> | undefined>(undefined);
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD': {
const items = [...state.items, action.item];
return { items, total: items.reduce((sum, i) => sum + i.price, 0) };
}
case 'REMOVE': {
const items = state.items.filter((i) => i.id !== action.id);
return { items, total: items.reduce((sum, i) => sum + i.price, 0) };
}
case 'CLEAR':
return { items: [], total: 0 };
default:
return state;
}
}
export function CartProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(cartReducer, { items: [], total: 0 });
const memoizedState = useMemo(() => state, [state]);
// dispatch conserva una referencia estable garantizada entre renders en React
return (
<CartStateContext.Provider value={memoizedState}>
<CartDispatchContext.Provider value={dispatch}>
{/* Los hijos se reciben por props children: React omite su reconciliación */}
{children}
</CartDispatchContext.Provider>
</CartStateContext.Provider>
);
}
// Hooks con tipado estricto y validación de invariantes de presencia en el árbol
export function useCartState(): CartState {
const context = useContext(CartStateContext);
if (!context) {
throw new Error('[useCartState] Violación de invariante: invocado fuera de <CartProvider>');
}
return context;
}
export function useCartDispatch(): React.Dispatch<CartAction> {
const context = useContext(CartDispatchContext);
if (!context) {
throw new Error('[useCartDispatch] Violación de invariante: invocado fuera de <CartProvider>');
}
return context;
}A partir de este diseño, el botón «Agregar al carrito» se suscribe exclusivamente a useCartDispatch(). Cuando el contenido del carrito mute, el botón no experimentará ningún re-renderizado parásito.
Cuando un contexto alberga un árbol de datos complejo, la división State/Dispatch resulta insuficiente. Se precisan selectores granulares sin dependencias externas:
'use client';
import React, { createContext, useContext, useRef, useSyncExternalStore, useCallback } from 'react';
export interface StoreApi<T> {
getState: () => T;
setState: (fn: (prev: T) => T) => void;
subscribe: (listener: () => void) => () => void;
}
export function createStoreContext<T>() {
const Context = createContext<StoreApi<T> | null>(null);
function Provider({
initialState,
children,
}: {
initialState: T;
children: React.ReactNode;
}) {
const storeRef = useRef<StoreApi<T> | null>(null);
if (!storeRef.current) {
let state = initialState;
const listeners = new Set<() => void>();
storeRef.current = {
getState: () => state,
setState: (updater) => {
state = updater(state);
listeners.forEach((listener) => listener());
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
return <Context.Provider value={storeRef.current}>{children}</Context.Provider>;
}
function useSelector<Selected>(selector: (state: T) => Selected): Selected {
const store = useContext(Context);
if (!store) {
throw new Error('[useSelector] El hook debe invocarse dentro de su Provider correspondiente');
}
const getSnapshot = useCallback(() => selector(store.getState()), [store, selector]);
return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
}
function useSetState(): StoreApi<T>['setState'] {
const store = useContext(Context);
if (!store) {
throw new Error('[useSetState] El hook debe invocarse dentro de su Provider correspondiente');
}
return store.setState;
}
return { Provider, useSelector, useSetState };
}El Provider genera un contenedor storeRef inmutable que alberga el estado, listeners y métodos de suscripción. La referencia de Context es estable para siempre.
El componente invoca useSelector(state => state.activeId). React se suscribe de manera directa mediante store.subscribe.
Se ejecuta setState(). El estado se actualiza en memoria y los oyentes registrados se notifican sincrónicamente.
React compara los resultados del selector mediante Object.is. ÚNICAMENTE se actualizan aquellos componentes cuya porción extraída varió.
¿Por qué <MyProvider>{children}</MyProvider> previene el re-renderizado de los componentes dentro de children al actualizar el estado interno de MyProvider, mientras que declarar <MyProvider><Child /></MyProvider> directamente en el cuerpo del padre fuerza la actualización de Child?
'use client';
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
export interface WizardData {
step: number;
customerEmail: string;
shippingAddress: string;
isCompleted: boolean;
}
export interface WizardActions {
goToNextStep: () => void;
goToPreviousStep: () => void;
updateData: (fields: Partial<Omit<WizardData, 'step' | 'isCompleted'>>) => void;
completeWizard: () => void;
}
const WizardStateContext = createContext<WizardData | null>(null);
const WizardActionsContext = createContext<WizardActions | null>(null);
export function WizardProvider({ children }: { children: React.ReactNode }) {
const [data, setData] = useState<WizardData>({
step: 1,
customerEmail: '',
shippingAddress: '',
isCompleted: false,
});
const goToNextStep = useCallback(() => {
setData((prev) => ({ ...prev, step: prev.step + 1 }));
}, []);
const goToPreviousStep = useCallback(() => {
setData((prev) => ({ ...prev, step: Math.max(1, prev.step - 1) }));
}, []);
const updateData = useCallback(
(fields: Partial<Omit<WizardData, 'step' | 'isCompleted'>>) => {
setData((prev) => ({ ...prev, ...fields }));
},
[]
);
const completeWizard = useCallback(() => {
setData((prev) => ({ ...prev, isCompleted: true }));
}, []);
// Objeto de acciones referencialmente estable: no se recrea jamás en la vida del provider
const actions = useMemo<WizardActions>(
() => ({
goToNextStep,
goToPreviousStep,
updateData,
completeWizard,
}),
[goToNextStep, goToPreviousStep, updateData, completeWizard]
);
return (
<WizardStateContext.Provider value={data}>
<WizardActionsContext.Provider value={actions}>
{/* children aislados de los re-renders internos del Provider */}
{children}
</WizardActionsContext.Provider>
</WizardStateContext.Provider>
);
}
export function useWizardState(): WizardData {
const ctx = useContext(WizardStateContext);
if (!ctx) {
throw new Error('[useWizardState] debe invocarse dentro de un <WizardProvider>');
}
return ctx;
}
export function useWizardActions(): WizardActions {
const ctx = useContext(WizardActionsContext);
if (!ctx) {
throw new Error('[useWizardActions] debe invocarse dentro de un <WizardProvider>');
}
return ctx;
}Declarar { theme, toggleTheme } directamente dentro de value sin useMemo genera una referencia nueva en memoria en cada ciclo de render del padre. Todos los componentes suscritos se re-renderizarán inútilmente aunque ni theme ni toggleTheme hayan sufrido cambios.
// ❌ ANTIPATRÓN: Se crea una nueva referencia en cada render del padre
export function BadThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('dark');
// ADVERTENCIA: ¡value altera su referencia en cada ciclo!
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
// ✅ PATRÓN DE PRODUCCIÓN: Referencia estabilizada mediante useMemo
export function GoodThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState('dark');
const contextValue = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}Guardar variables que mutan decenas de veces por segundo (cronómetros, coordenadas de scroll, animaciones) dentro de un Context bloquea la interfaz de usuario, puesto que React se ve forzado a recorrer sincrónicamente todo el árbol de componentes suscritos en cada fotograma.
// ❌ ANTIPATRÓN: Un temporizador en Context satura el hilo principal
function TimerProvider({ children }: { children: React.ReactNode }) {
const [ms, setMs] = useState(0);
useEffect(() => {
const id = setInterval(() => setMs((v) => v + 10), 10);
return () => clearInterval(id);
}, []);
// ¡Todos los suscriptores se actualizarán 100 veces por segundo!
return <TimerContext.Provider value={ms}>{children}</TimerContext.Provider>;
}
// ✅ PATRÓN DE PRODUCCIÓN: Emplear Zustand transient updates o referencias directas al DOMDefinir componentes secundarios de forma estática dentro del cuerpo del Provider causa que dichos elementos se recreen en cada actualización del proveedor, incluso cuando no hagan uso de useContext.
// ❌ ANTIPATRÓN: Componentes hijos acoplados dentro del cuerpo del provider
function BadParentProvider() {
const [state, setState] = useState(0);
return (
<MyContext.Provider value={state}>
<HeavySidebar /> {/* ¡SE RE-RENDERIZA INCONDICIONALMENTE! */}
<HeavyContent /> {/* ¡SE RE-RENDERIZA INCONDICIONALMENTE! */}
</MyContext.Provider>
);
}
// ✅ PATRÓN DE PRODUCCIÓN: Suministrados a través de props children
function GoodParentProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState(0);
return <MyContext.Provider value={state}>{children}</MyContext.Provider>;
}Ejecutar useContext fuera de su correspondiente Provider retorna undefined. Omitir una verificación explícita causa excepciones opacas y difíciles de depurar en puntos posteriores del código, como Cannot read properties of undefined.
// ❌ ANTIPATRÓN: Un undefined inadvertido desemboca en fallos inesperados
export function useAuth() {
return useContext(AuthContext); // Retornará undefined si falta <AuthProvider>
}
// ✅ PATRÓN DE PRODUCCIÓN: Comprobación estricta de invariantes
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('[useAuth] Error crítico: el componente debe residir dentro de <AuthProvider>');
}
return context;
}