En esta página

Context API

14 min lectura TextoCap. 3 — Patrones y navegación

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
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.

El contexto re-renderiza todos los consumidores cuando el valor cambia
Cada vez que el valor del Provider cambia, todos los componentes que consumen ese contexto se re-renderizan. Para evitar renders innecesarios, divide contextos grandes en contextos más pequeños y memoiza el valor del Provider con useMemo cuando sea necesario.
Context no reemplaza a Zustand o Redux para estado complejo
El Context API es ideal para datos globales que cambian poco (tema, idioma, usuario autenticado). Para estado que cambia frecuentemente o con actualizaciones selectivas (solo re-renderizar el componente que necesita X), usa Zustand o Redux Toolkit.
Patrón de contexto seguro con hook personalizado
Siempre envuelve useContext en un hook personalizado que valide que el contexto no sea undefined. Esto da mensajes de error claros en lugar del críptico TypeError cuando alguien usa el hook fuera del Provider.