En esta página
Context API
El problema del prop drilling
En una aplicación React, los datos fluyen de padre a hijo mediante props. Cuando un dato necesita llegar a componentes muy anidados, se crea el prop drilling: pasar props a través de componentes intermedios que no las usan.
App (tiene usuario)
→ Layout (pasa usuario)
→ Sidebar (pasa usuario)
→ NavUsuario (pasa usuario)
→ AvatarUsuario (¡finalmente usa usuario!)Layout, Sidebar y NavUsuario solo sirven de intermediarios. Esto es prop drilling y hace el código frágil y difícil de mantener.
createContext — la solución
createContext crea un canal de comunicación que permite que cualquier componente descendiente acceda a los datos sin prop drilling:
import { createContext } from 'react';
// El valor genérico T define qué tipo de datos transporta el contexto
const MiContexto = createContext<string>('valor por defecto');El valor por defecto se usa solo cuando un componente consume el contexto sin estar dentro de un Provider. En la práctica, siempre estarás dentro de un Provider, así que el valor por defecto raramente importa.
El patrón recomendado con TypeScript
El patrón más robusto para contextos tipados en TypeScript:
// 1. Tipo del contexto
interface ConfigContextoTipo {
idioma: 'es' | 'en';
moneda: 'USD' | 'BOB' | 'EUR';
setIdioma: (idioma: 'es' | 'en') => void;
setMoneda: (moneda: 'USD' | 'BOB' | 'EUR') => void;
}
// 2. Contexto con undefined como valor inicial
const ConfigContexto = createContext<ConfigContextoTipo | undefined>(undefined);
// 3. Hook con validación (el patrón más seguro)
export function useConfig(): ConfigContextoTipo {
const ctx = useContext(ConfigContexto);
if (ctx === undefined) {
throw new Error('useConfig debe usarse dentro de <ConfigProvider>');
}
return ctx;
}
// 4. Provider como componente separado
export function ConfigProvider({ children }: { children: React.ReactNode }) {
const [idioma, setIdioma] = useState<'es' | 'en'>('es');
const [moneda, setMoneda] = useState<'USD' | 'BOB' | 'EUR'>('USD');
return (
<ConfigContexto.Provider value={{ idioma, setIdioma, moneda, setMoneda }}>
{children}
</ConfigContexto.Provider>
);
}Múltiples contextos y composición
En aplicaciones reales, tendrás varios contextos. La práctica es componerlos:
// Cada contexto tiene una responsabilidad única
function Providers({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<TemaProvider>
<ConfigProvider>
<NotificacionProvider>
{children}
</NotificacionProvider>
</ConfigProvider>
</TemaProvider>
</AuthProvider>
);
}
// main.tsx
root.render(
<Providers>
<App />
</Providers>
);Optimizar renders del contexto
Cuando el valor del Provider cambia, todos sus consumidores se re-renderizan. Para contextos con datos que cambian frecuentemente, separa los datos de las funciones:
// ❌ Un solo contexto: cualquier cambio re-renderiza todos los consumidores
const TodoContexto = createContext<{ datos: Dato[]; acciones: Acciones } | undefined>(undefined);
// ✅ Dos contextos: las acciones son estables, solo los datos re-renderizan
const DatosContexto = createContext<Dato[] | undefined>(undefined);
const AccionesContexto = createContext<Acciones | undefined>(undefined);
function Provider({ children }: { children: React.ReactNode }) {
const [datos, setDatos] = useState<Dato[]>([]);
// useCallback: las acciones mantienen la misma referencia entre renders
const agregar = useCallback((dato: Dato) => {
setDatos((prev) => [...prev, dato]);
}, []);
const eliminar = useCallback((id: string) => {
setDatos((prev) => prev.filter((d) => d.id !== id));
}, []);
const acciones = useMemo(() => ({ agregar, eliminar }), [agregar, eliminar]);
return (
<DatosContexto.Provider value={datos}>
<AccionesContexto.Provider value={acciones}>
{children}
</AccionesContexto.Provider>
</DatosContexto.Provider>
);
}Contexto de notificaciones — ejemplo completo
interface Notificacion {
id: string;
tipo: 'exito' | 'error' | 'info';
mensaje: string;
}
interface NotifContextoTipo {
notificaciones: Notificacion[];
mostrar: (tipo: Notificacion['tipo'], mensaje: string) => void;
cerrar: (id: string) => void;
}
const NotifContexto = createContext<NotifContextoTipo | undefined>(undefined);
export function useNotificaciones(): NotifContextoTipo {
const ctx = useContext(NotifContexto);
if (!ctx) throw new Error('useNotificaciones debe estar dentro de <NotifProvider>');
return ctx;
}
export function NotifProvider({ children }: { children: React.ReactNode }) {
const [notificaciones, setNotificaciones] = useState<Notificacion[]>([]);
const mostrar = useCallback((tipo: Notificacion['tipo'], mensaje: string) => {
const id = crypto.randomUUID();
setNotificaciones((prev) => [...prev, { id, tipo, mensaje }]);
setTimeout(() => {
setNotificaciones((prev) => prev.filter((n) => n.id !== id));
}, 4000);
}, []);
const cerrar = useCallback((id: string) => {
setNotificaciones((prev) => prev.filter((n) => n.id !== id));
}, []);
return (
<NotifContexto.Provider value={{ notificaciones, mostrar, cerrar }}>
{children}
<div aria-live="polite" className="notificaciones-contenedor">
{notificaciones.map((n) => (
<div key={n.id} className={`notif notif-${n.tipo}`} role="alert">
{n.mensaje}
<button type="button" onClick={() => cerrar(n.id)} aria-label="Cerrar notificación">✕</button>
</div>
))}
</div>
</NotifContexto.Provider>
);
}Context API vs Zustand
| Criterio | Context API | Zustand |
|---|---|---|
| Configuración | Mínima | Mínima |
| Re-renders | Todos los consumidores | Solo selectores afectados |
| Devtools | No | Sí |
| Middleware | No | Sí (persist, devtools) |
| Ideal para | Tema, idioma, auth | Estado complejo, performance |
Usa Context API para datos globales que cambian poco. Usa Zustand cuando el rendimiento de los re-renders sea un problema o cuando necesites middleware como persistencia.
Inicia sesión para guardar tu progreso