Context API: Creando proveedores de estado

Lectura
25 min~7 min lectura

Concepto clave

La Context API es un mecanismo de React para compartir datos entre componentes sin necesidad de pasar props manualmente a través de múltiples niveles (prop drilling). Imagina que tienes una aplicación de comercio electrónico con un carrito de compras. El carrito debe ser accesible desde la barra de navegación, la página de productos y la página de pago. Sin Context, tendrías que pasar el estado del carrito como prop desde el componente raíz hasta cada componente que lo necesite, lo que resulta en código verboso y difícil de mantener. Con Context, puedes crear un proveedor que envuelva la aplicación y un consumidor (o el hook useContext) que acceda al valor desde cualquier componente hijo.

La analogía del mundo real: piensa en un sistema de megafonía en un edificio. El proveedor es el altavoz central que emite un mensaje (el estado), y cualquier persona en el edificio (componente) puede escucharlo sin necesidad de que alguien le transmita el mensaje individualmente. Así, el estado global se vuelve accesible de forma eficiente y desacoplada.

Cómo funciona en la práctica

Para crear un proveedor de estado con Context API, sigues estos pasos:

  1. Crear el contexto con React.createContext(). Esto devuelve un objeto con dos componentes: Provider y Consumer.
  2. Crear un componente proveedor que envuelva a los hijos y pase un valor a través del Provider. Normalmente, este proveedor maneja el estado con useState o useReducer.
  3. Consumir el contexto en cualquier componente hijo usando el hook useContext o el componente Consumer.

Ejemplo paso a paso: Supongamos que queremos un contexto de tema (claro/oscuro). Primero, creamos el archivo ThemeContext.js:

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

export const ThemeContext = createContext();

export const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

Luego, en el componente raíz (App.js), envolvemos la aplicación con el proveedor:

import React from 'react';
import { ThemeProvider } from './ThemeContext';
import Header from './Header';

function App() {
  return (
    <ThemeProvider>
      <Header />
    </ThemeProvider>
  );
}

export default App;

Finalmente, en el componente Header.js, consumimos el contexto:

import React, { useContext } from 'react';
import { ThemeContext } from './ThemeContext';

function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <header style={{ background: theme === 'light' ? '#fff' : '#333' }}>
      <button onClick={toggleTheme}>Cambiar tema</button>
    </header>
  );
}

export default Header;

Código en acción

Aquí tienes un ejemplo completo y funcional de un proveedor de carrito de compras. Crea un archivo CartContext.js:

import React, { createContext, useReducer, useContext } from 'react';

const CartContext = createContext();

const cartReducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return [...state, action.payload];
    case 'REMOVE_ITEM':
      return state.filter(item => item.id !== action.payload.id);
    default:
      return state;
  }
};

export const CartProvider = ({ children }) => {
  const [cart, dispatch] = useReducer(cartReducer, []);

  const addItem = (item) => dispatch({ type: 'ADD_ITEM', payload: item });
  const removeItem = (item) => dispatch({ type: 'REMOVE_ITEM', payload: item });

  return (
    <CartContext.Provider value={{ cart, addItem, removeItem }}>
      {children}
    </CartContext.Provider>
  );
};

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

Luego, en un componente ProductList.js:

import React from 'react';
import { useCart } from './CartContext';

const products = [
  { id: 1, name: 'Camiseta', price: 20 },
  { id: 2, name: 'Pantalón', price: 40 },
];

function ProductList() {
  const { addItem } = useCart();

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>
          {product.name} - ${product.price}
          <button onClick={() => addItem(product)}>Agregar al carrito</button>
        </li>
      ))}
    </ul>
  );
}

export default ProductList;

Y en Cart.js:

import React from 'react';
import { useCart } from './CartContext';

function Cart() {
  const { cart, removeItem } = useCart();

  return (
    <ul>
      {cart.map(item => (
        <li key={item.id}>
          {item.name} - ${item.price}
          <button onClick={() => removeItem(item)}>Eliminar</button>
        </li>
      ))}
    </ul>
  );
}

export default Cart;

Este ejemplo muestra cómo crear un proveedor con useReducer para manejar lógica más compleja, y cómo exponer un hook personalizado useCart para facilitar el consumo.

Errores comunes

  1. No envolver la aplicación con el proveedor: Si olvidas colocar el Provider en un nivel superior, los componentes que intenten consumir el contexto lanzarán un error o recibirán undefined. Solución: Siempre verifica que el proveedor esté en un ancestro común.
  2. Crear un nuevo contexto cada render: Si defines el contexto dentro de un componente que se renderiza frecuentemente, se creará un nuevo objeto de contexto cada vez, causando que los consumidores se vuelvan a renderizar innecesariamente. Solución: Define el contexto fuera del componente, en un archivo separado.
  3. Pasar objetos o funciones sin memoizar: Si el valor del proveedor es un objeto o función creado en cada render, todos los consumidores se re-renderizarán aunque el estado no haya cambiado. Solución: Usa useMemo o useCallback para estabilizar las referencias.
  4. Usar Context para todo el estado global: Context no está diseñado para reemplazar bibliotecas de estado global como Redux en aplicaciones grandes. Para estados que cambian frecuentemente, Context puede causar renders innecesarios en toda la aplicación. Solución: Evalúa si realmente necesitas estado global o si puedes usar estado local o lifting state up.
  5. No validar que el contexto esté disponible: Al usar useContext, si el componente está fuera del proveedor, obtendrá el valor por defecto (que suele ser undefined). Solución: Crea un hook personalizado que lance un error si no hay proveedor, como en el ejemplo de useCart.

Checklist de dominio

  • [ ] He creado un contexto con createContext y lo he exportado.
  • [ ] He creado un componente proveedor que maneja el estado con useState o useReducer.
  • [ ] He envuelto la aplicación (o parte de ella) con el proveedor.
  • [ ] He consumido el contexto en al menos un componente hijo usando useContext.
  • [ ] He creado un hook personalizado (ej. useCart) para consumir el contexto con validación.
  • [ ] He verificado que los consumidores no se re-rendericen innecesariamente (usando React DevTools o console.log).
  • [ ] He evitado pasar objetos/funciones sin memoizar en el valor del proveedor.

Para verificar que tu implementación es correcta, abre las herramientas de desarrollo de React y observa el árbol de componentes. Deberías ver el proveedor como un nodo en la jerarquía, y los consumidores deberían mostrar el valor del contexto sin errores.

Crear un proveedor de autenticación con Context API

Objetivo

Implementar un contexto de autenticación que permita a los usuarios iniciar sesión y cerrar sesión, y que exponga el estado de autenticación a toda la aplicación.

Entregable

Un archivo AuthContext.js que contenga el contexto, el proveedor y un hook personalizado useAuth. Además, un componente LoginButton.js que use el hook para mostrar un botón de inicio/cierre de sesión.

Pasos

  1. Crea un archivo AuthContext.js.
  2. Define un contexto con createContext.
  3. Crea un componente AuthProvider que gestione el estado user (inicialmente null) y una función login que reciba un objeto usuario y lo establezca, y una función logout que lo ponga a null.
  4. Pasa { user, login, logout } como valor al Provider.
  5. Crea un hook useAuth que llame a useContext y lance un error si no hay proveedor.
  6. Exporta AuthProvider y useAuth.
  7. En un componente LoginButton.js, usa useAuth para mostrar un botón: si user es null, muestra "Iniciar sesión" y llama a login con un objeto de prueba; si hay usuario, muestra "Cerrar sesión" y llama a logout.
  8. Envuelve tu componente App con AuthProvider y renderiza LoginButton.

Criterios de evaluación (mini-rúbrica)

  • Correctitud: El contexto se consume correctamente y el estado se actualiza al hacer clic.
  • Validación: El hook useAuth lanza un error si se usa fuera del proveedor.
  • Separación: El contexto está en un archivo separado y el proveedor envuelve la aplicación.
  • Funcionalidad: El botón cambia su texto y comportamiento según el estado de autenticación.
  • Buenas prácticas: No hay renders innecesarios (verifica con console.log en el cuerpo del componente).
Pistas
  • Usa createContext sin valor por defecto o con null para forzar el uso del proveedor.
  • Para simular un inicio de sesión, puedes usar un objeto usuario fijo como { name: 'Usuario', email: '[email protected]' }.
  • Recuerda envolver la aplicación con AuthProvider en el archivo index.js o App.js.