Context API: Estado Global sin Complejidades

Lectura
30 min~8 min lectura
CONCEPTO CLAVE: Context API es un mecanismo incorporado en React que permite pasar datos a través del árbol de componentes sin necesidad de pasar "props" manualmente en cada nivel. Es la solución nativa de React para el estado global.

¿Qué Problema Resuelve Context API?

Imaginemos una aplicación donde tenemos un tema visual (modo claro/oscuro) que debe aplicarse en docenas de componentes dispersos por toda la aplicación. Sin Context, tendríamos que pasar esa prop a través de cada componente intermedio, aunque ese componente no la use directamente:

// Sin Context: Prop Drilling (perforación de props)
function App() {
  return <Header theme="dark" />;
}

function Header({ theme }) {
  return (
    <div>
      <Logo theme={theme} />
      <Nav theme={theme} />
      <UserMenu theme={theme} />
    </div>
  );
}

function Nav({ theme }) {
  return (
    <nav>
      <Link theme={theme} />
      <Link theme={theme} />
    </nav>
  );
}

// ¡El componente Link recibe theme aunque solo le importe el color del texto!
function Link({ theme }) {
  return <a style={{ color: theme === 'dark' ? 'white' : 'black' }}>...</a>;
}

Este patrón se llama prop drilling y genera código redundante, difícil de mantener y propenso a errores cuando necesitas cambiar la estructura de datos.

📌 Context API rompe esta cadena: en lugar de pasar datos por cada nivel, cualquier componente puede "suscribirse" directamente al contexto que necesita, sin importar dónde esté en el árbol de componentes.

Los Tres Pasos Fundamentales

  1. Crear el Context: Usando React.createContext(), definimos un "tubo" de datos con un valor inicial.
  2. Proveer el Context: Envolvemos la parte de nuestra aplicación que necesita acceso a esos datos con el componente <MiContext.Provider>.
  3. Consumir el Context: Cualquier componente hijo puede acceder a los datos usando el hook useContext() o el componente <MiContext.Consumer>.

Creando Nuestro Primer Context: Un Sistema de Autenticación

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

// 1. Crear el Context con un valor por defecto
const AuthContext = createContext({
  user: null,
  login: () => {},
  logout: () => {},
  isAuthenticated: false
});

// 2. Crear el Provider (componente queprovee los datos)
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  
  const login = (userData) => {
    setUser(userData);
  };
  
  const logout = () => {
    setUser(null);
  };
  
  const value = {
    user,
    login,
    logout,
    isAuthenticated: !!user
  };
  
  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// 3. Hook personalizado para consumir el context
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth debe usarse dentro de AuthProvider');
  }
  return context;
}

// 4. Usar el hook en cualquier componente
export default function ProfileMenu() {
  const { user, logout } = useAuth();
  
  if (!user) return <Link to="/login">Iniciar Sesión</Link>;
  
  return (
    <div>
      <span>Hola, {user.name}</span>
      <button onClick={logout}>Cerrar Sesión</button>
    </div>
  );
}
💡 Buena práctica: Crea un hook personalizado como useAuth() que envuelva useContext(). Esto facilita cambios futuros y proporciona mensajes de error más claros si el context no está disponible.

Patrón Avanzado: Múltiples Contexts Anidados

En aplicaciones reales, tendrás múltiples contextos que coexisten. Aquí un ejemplo de cómo estructurar un sistema de theming completo:

// ThemeContext.js
const ThemeContext = createContext();

const themes = {
  light: {
    background: '#ffffff',
    text: '#333333',
    primary: '#007bff',
    secondary: '#6c757d'
  },
  dark: {
    background: '#1a1a2e',
    text: '#eaeaea',
    primary: '#4dabf7',
    secondary: '#868e96'
  }
};

export function ThemeProvider({ children }) {
  const [themeName, setThemeName] = useState('light');
  
  const toggleTheme = () => {
    setThemeName(prev => prev === 'light' ? 'dark' : 'light');
  };
  
  const value = {
    theme: themes[themeName],
    themeName,
    toggleTheme
  };
  
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

export const useTheme = () => useContext(ThemeContext);

// Uso combinado con AuthContext
function Dashboard() {
  const { theme } = useTheme();
  const { user } = useAuth();
  
  return (
    <div style={{ background: theme.background, color: theme.text }}>
      <h1>Bienvenido, {user.name}</h1>
      <Button variant="primary">Principal</Button>
    </div>
  );
}
⚠️ Precaución: No anides demasiados Contexts en un solo componente. Si necesitas más de 3-4 contexts en un componente, probablemente necesitas refactorizar y crear un context más especializado o dividir tu aplicación en componentes más pequeños.

Optimización de Rendimiento

Context tiene un problema de rendimiento conocido: cualquier cambio en el valor del Provider hace que TODOS los componentes consumidores se re-rendericen. Para evitar esto, existen técnicas:

// ❌ Mal: Valor en línea causa re-renders innecesarios
function Parent() {
  return (
    <MyContext.Provider value={{ count: count, setCount: setCount }}>
      <Child />
    </MyContext.Provider>
  );
}

// ✅ Bien: Memoizar el valor
import { useMemo } from 'react';

function Parent() {
  const value = useMemo(() => ({
    count,
    setCount
  }), [count]);
  
  return (
    <MyContext.Provider value={value}>
      <Child />
    </MyContext.Provider>
  );
}

// ✅ Alternativa: Dividir el Provider
function CountProvider({ children }) {
  const [count, setCount] = useState(0);
  return (
    <CountContext.Provider value={count}>
      <SetCountContext.Provider value={setCount}>
        {children}
      </SetCountContext.Provider>
    </CountContext.Provider>
  );
}
💡 Pro tip: La librería zustand o jotai ofrecen la misma funcionalidad de estado global pero con mejor rendimiento automático. Context API es ideal para datos que cambian poco (tema, usuario, idioma), pero para datos que cambian frecuentemente, considera alternativas.

Patrón de Contexto Escalable: CartContext

Veamos un ejemplo completo y práctico de un carrito de compras:

const CartContext = createContext();

function CartProvider({ children }) {
  const [items, setItems] = useState([]);
  const [isOpen, setIsOpen] = useState(false);
  
  const addItem = (product) => {
    setItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      if (existing) {
        return prev.map(item => 
          item.id === product.id 
            ? { ...item, quantity: item.quantity + 1 }
            : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  };
  
  const removeItem = (productId) => {
    setItems(prev => prev.filter(item => item.id !== productId));
  };
  
  const updateQuantity = (productId, quantity) => {
    if (quantity <= 0) {
      removeItem(productId);
      return;
    }
    setItems(prev => prev.map(item =>
      item.id === productId ? { ...item, quantity } : item
    ));
  };
  
  const clearCart = () => setItems([]);
  
  const total = items.reduce((sum, item) => 
    sum + (item.price * item.quantity), 0
  );
  
  const totalItems = items.reduce((sum, item) => 
    sum + item.quantity, 0
  );
  
  const value = {
    items,
    addItem,
    removeItem,
    updateQuantity,
    clearCart,
    total,
    totalItems,
    isOpen,
    openCart: () => setIsOpen(true),
    closeCart: () => setIsOpen(false)
  };
  
  return (
    <CartContext.Provider value={value}>
      {children}
    </CartContext.Provider>
  );
}

export const useCart = () => {
  const context = useContext(CartContext);
  if (!context) {
    throw new Error('useCart debe usarse dentro de CartProvider');
  }
  return context;
};

Cuándo Usar Context y Cuándo No

Usa Context ✅ Usa Props o State Local ❌
Datos de autenticación (usuario actual) Datos específicos de un solo componente
Configuración de tema (claro/oscuro) Estado de un formulario temporal
Idioma/región de la aplicación Items en un dropdown
Datos que cambian raramente (settings) Datos que cambian frecuentemente (posiciones, scrolls)
Estado global compartido por muchos componentes Estado aislado de un componente hijo directo
Ver más: Estructura de archivos recomendada
src/
├── contexts/
│   ├── AuthContext.jsx    # Context + Provider + Hook
│   ├── ThemeContext.jsx
│   └── CartContext.jsx
├── components/
│   ├── Button.jsx
│   ├── Header.jsx
│   └── ProductCard.jsx
├── hooks/
│   └── useLocalStorage.js
└── App.jsx

// En App.jsx:
function App() {
  return (
    <AuthProvider>
      <ThemeProvider>
        <CartProvider>
          <Router>
            <Layout />
          </Router>
        </CartProvider>
      </ThemeProvider>
    </AuthProvider>
  );
}

Este patrón de anidar providers funciona perfectamente porque cada provider solo necesita conocer su propio contexto, no el de sus "padres".

"Context es como el agua: puede fluir a través de cualquier contenedor sin romperlo, pero necesitas canales bien diseñados para que fluya donde debe." — Principio de diseño de React

Errores Comunes y Cómo Evitarlos

📌 Error #1: Crear múltiples contexts cuando uno sería suficiente. Si tienes UserContext, UserDispatchContext y UserStatusContext, probablemente deberían ser uno solo.
⚠️ Error #2: Olvidar que Context no es mágico. Actualizar un context fuerza re-render en todos los componentes que lo consumen. Si tu app tiene problemas de rendimiento, Context puede ser el culpable.
💡 Error #3: No proporcionar un valor inicial significativo. El default value de createContext() solo se usa cuando el componente está fuera del Provider. Siempre usa el Provider.

Resumen y Próximos Pasos

Context API es una herramienta poderosa pero especializada. Ahora que conoces:

  • Cómo crear contextos con createContext()
  • Cómo proveeer datos con Providers
  • Cómo consumirlos con useContext()
  • Técnicas de optimización de rendimiento
  • Patrones escalables para aplicaciones reales

Estás listo para implementarlo en tus proyectos. Recuerda: usa Context para datos que genuinamente necesitan ser globales, no para todo. El prop drilling en 2-3 niveles es perfectamente aceptable y mantiene el código más predecible.

🧠 Quiz

¿Cuál es la principal desventaja de Context API en términos de rendimiento?

  • A) Consume mucha memoria RAM
  • B) Hace el código más difícil de leer
  • C) Cualquier cambio en el Provider causa re-render en todos los componentes consumidores
  • D) No funciona con hooks personalizados
✅ Respuesta: C. Context no tiene mecanismo de memoización automático, así que cuando el valor del Provider cambia, todos los componentes que usan useContext se re-renderizan, incluso si solo usan parte de los datos.
🧠 Quiz

¿Por qué es mejor práctica crear un hook personalizado como useAuth() en lugar de usar useContext(AuthContext) directamente en los componentes?

  • A) Es más rápido en ejecución
  • B) Permite cambiar el estado global sin re-renders
  • C) Proporciona un error claro si se usa fuera del Provider y facilita cambiar la implementación
  • D) Reduce el número de líneas de código
✅ Respuesta: C. El hook personalizado envuelve useContext y puede lanzar un error descriptivo si se intenta usar fuera del Provider. Además, si en el futuro necesitas cambiar cómo funciona la autenticación, solo modificas el hook, no todos los componentes.