TechLead

React Context API

Compartición de estado integrada usando Context API de React y el hook useContext

React Context API

Context proporciona una forma de pasar datos a través del árbol de componentes sin tener que pasar props manualmente en cada nivel. Está integrado en React y es perfecto para compartir datos "globales" como temas, autenticación de usuario o preferencias de idioma.

Cuándo Usar Context

  • Tema — Modo claro/oscuro en toda la app
  • Autenticación de Usuario — Datos del usuario actual
  • Locale — Preferencias de idioma y formato
  • Feature Flags — Activar/desactivar funciones
  • Evitar Prop Drilling — Datos necesarios por muchos componentes

Creando y Usando Context

import { createContext, useContext, useState } from 'react';

// 1. Crear el context con un valor por defecto
const ThemeContext = createContext('light');

// 2. Crear un componente provider
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  // El objeto value contiene todo lo que los consumidores necesitan
  const value = { theme, toggleTheme };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. Hook personalizado para consumo fácil
function useTheme() {
  const context = useContext(ThemeContext);
  if (context === undefined) {
    throw new Error('useTheme debe usarse dentro de un ThemeProvider');
  }
  return context;
}

// 4. Usar en componentes
function Header() {
  const { theme, toggleTheme } = useTheme();
  
  return (
    <header className={theme === 'dark' ? 'bg-gray-900' : 'bg-white'}>
      <button onClick={toggleTheme}>
        Cambiar a modo {theme === 'light' ? 'oscuro' : 'claro'}
      </button>
    </header>
  );
}

// 5. Envolver tu app con el provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <Footer />
    </ThemeProvider>
  );
}

Ejemplo de Context de Autenticación

import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [usuario, setUsuario] = useState(null);
  const [cargando, setCargando] = useState(true);
  
  useEffect(() => {
    // Verificar sesión existente al montar
    const verificarAuth = async () => {
      try {
        const response = await fetch('/api/auth/me');
        if (response.ok) {
          const datosUsuario = await response.json();
          setUsuario(datosUsuario);
        }
      } catch (error) {
        console.error('Verificación de auth falló:', error);
      } finally {
        setCargando(false);
      }
    };
    
    verificarAuth();
  }, []);
  
  const iniciarSesion = async (email, contrasena) => {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, contrasena }),
    });
    
    if (response.ok) {
      const datosUsuario = await response.json();
      setUsuario(datosUsuario);
      return { exito: true };
    }
    
    return { exito: false, error: 'Credenciales inválidas' };
  };
  
  const cerrarSesion = async () => {
    await fetch('/api/auth/logout', { method: 'POST' });
    setUsuario(null);
  };
  
  const value = {
    usuario,
    cargando,
    iniciarSesion,
    cerrarSesion,
    estaAutenticado: !!usuario,
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth debe usarse dentro de un AuthProvider');
  }
  return context;
}

// Uso en componentes
function PaginaPerfil() {
  const { usuario, cargando, cerrarSesion } = useAuth();
  
  if (cargando) return <div>Cargando...</div>;
  if (!usuario) return <div>Por favor inicia sesión</div>;
  
  return (
    <div>
      <h1>¡Bienvenido, {usuario.nombre}!</h1>
      <button onClick={cerrarSesion}>Cerrar Sesión</button>
    </div>
  );
}

Múltiples Contexts

// Componer múltiples providers
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          <NotificationProvider>
            <Router>
              <AppContent />
            </Router>
          </NotificationProvider>
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

// O crear un provider combinado
function AppProviders({ children }) {
  return (
    <AuthProvider>
      <ThemeProvider>
        <LanguageProvider>
          {children}
        </LanguageProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

Optimizando el Rendimiento de Context

import { createContext, useContext, useState, useMemo, useCallback } from 'react';

// Problema: La referencia del objeto cambia en cada render
function BadProvider({ children }) {
  const [cuenta, setCuenta] = useState(0);
  
  // ❌ Nuevo objeto cada render = re-renders innecesarios
  const value = { cuenta, setCuenta };
  
  return <Context.Provider value={value}>{children}</Context.Provider>;
}

// Solución: Memorizar el objeto value
function GoodProvider({ children }) {
  const [cuenta, setCuenta] = useState(0);
  
  // ✅ Memorizar el value
  const value = useMemo(() => ({ 
    cuenta, 
    incrementar: () => setCuenta(c => c + 1),
    decrementar: () => setCuenta(c => c - 1),
  }), [cuenta]);
  
  return <Context.Provider value={value}>{children}</Context.Provider>;
}

// Mejor: Dividir en contexts separados
const CountContext = createContext(0);
const CountActionsContext = createContext(null);

function OptimizedProvider({ children }) {
  const [cuenta, setCuenta] = useState(0);
  
  // Las acciones nunca cambian, así que los componentes usando solo acciones no se re-renderizarán
  const acciones = useMemo(() => ({
    incrementar: () => setCuenta(c => c + 1),
    decrementar: () => setCuenta(c => c - 1),
  }), []);
  
  return (
    <CountContext.Provider value={cuenta}>
      <CountActionsContext.Provider value={acciones}>
        {children}
      </CountActionsContext.Provider>
    </CountContext.Provider>
  );
}

// Ahora los componentes pueden suscribirse solo a lo que necesitan
function Display() {
  const cuenta = useContext(CountContext); // Se re-renderiza cuando cuenta cambia
  return <span>{cuenta}</span>;
}

function Buttons() {
  const { incrementar, decrementar } = useContext(CountActionsContext);
  // ¡Nunca se re-renderiza cuando cuenta cambia!
  return (
    <div>
      <button onClick={decrementar}>-</button>
      <button onClick={incrementar}>+</button>
    </div>
  );
}

Pros y Contras de Context API

Pros Contras
Integrado en React, sin dependencias extra Todos los consumidores se re-renderizan en cualquier cambio de valor
API simple, fácil de entender Sin devtools integradas
Perfecto para actualizaciones de baja frecuencia Puede volverse verboso con múltiples contexts
Funciona con SSR desde el inicio Problemas de rendimiento con actualizaciones frecuentes

💡 Mejores Prácticas

  • • Siempre crea un hook personalizado (useTheme, useAuth) para consumir el context
  • • Lanza un error si el hook se usa fuera del provider
  • • Mantén los contexts pequeños y enfocados en una sola preocupación
  • • Memoriza el objeto value para prevenir re-renders innecesarios
  • • Considera bibliotecas externas para actualizaciones frecuentes