DEV Community

Cover image for 🔹 `useContext` – Compartir estado sin props (3/8)
Pedro Alvarado
Pedro Alvarado

Posted on

🔹 `useContext` – Compartir estado sin props (3/8)

📋 Tabla de contenidos

  1. useState – El fundamento del estado
  2. useEffect – Efectos secundarios y ciclo de vida
  3. useContext – Compartir estado sin props
  4. useReducer – Lógica de Estado Compleja y predecible
  5. useRef – Referencias, Almacenamiento y Escape
  6. useMemo y useCallback – Optimizando el Rendimiento
  7. Custom Hooks – Creando Lógica Reutilizable
  8. Errores Comunes y Soluciones

¿Qué es y para qué sirve?

useContext resuelve un problema muy común en React llamado "prop drilling". Esto ocurre cuando tienes que pasar props a través de múltiples niveles de componentes anidados, incluso si los componentes intermedios no las necesitan.

useContext te permite crear un "almacén" de datos global o semi-global al que cualquier componente dentro de un árbol específico puede acceder directamente, sin necesidad de recibirlo por props.

Úsalo para:

  • Gestionar el tema de la aplicación (oscuro/claro).
  • Información del usuario autenticado.
  • Configuraciones de idioma.
  • Estado de un carrito de compras.

Sintaxis y flujo de trabajo

El uso de Context API se divide en 3 pasos:

  1. Crear el Contexto:

    // theme-context.js
    import { createContext } from 'react';
    // El valor 'light' es el valor por defecto, solo se usa si un
    // componente intenta consumir el contexto sin un Provider por encima.
    export const ThemeContext = createContext('light');
    
  2. Proveer el Contexto:
    En algún lugar arriba en tu árbol de componentes (como App.js), envuelve a los componentes hijos con el Provider del contexto y pásale un value.

    // App.js
    import { ThemeContext } from './theme-context';
    
    function App() {
      const [theme, setTheme] = useState('dark');
      // Todos los componentes dentro de este Provider podrán acceder al valor 'dark'.
      return (
        <ThemeContext.Provider value={{ theme, setTheme }}>
          <Layout />
        </ThemeContext.Provider>
      );
    }
    
  3. Consumir el Contexto:
    Cualquier componente hijo puede usar el hook useContext para leer (y en este caso, modificar) el valor.

    // Button.js
    import { useContext } from 'react';
    import { ThemeContext } from './theme-context';
    
    function Button() {
      const { theme, setTheme } = useContext(ThemeContext);
      const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
    
      return <button onClick={toggleTheme}>Cambiar a tema {theme === 'light' ? 'oscuro' : 'claro'}</button>;
    }
    

Ejemplo práctico detallado: proveedor de autenticación

Vamos a crear un sistema simple para manejar el estado de autenticación de un usuario en toda la app.

1. Crear el AuthContext.js

// contexts/AuthContext.js
import React, { createContext, useState, useContext } from 'react';

// 1. Crear el contexto
const AuthContext = createContext(null);

// 2. Crear el Proveedor (un componente personalizado)
export function AuthProvider({ children }) {
  const [user, setUser] = useState(null); // El usuario no está logueado por defecto

  const login = (username) => setUser({ name: username });
  const logout = () => setUser(null);

  // Pasamos tanto el estado como las funciones para modificarlo
  const value = { user, login, logout };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// 3. Crear un Custom Hook para consumir el contexto (buena práctica)
export function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined) {
    throw new Error('useAuth debe ser usado dentro de un AuthProvider');
  }
  return context;
}
Enter fullscreen mode Exit fullscreen mode

2. Envolver la App con AuthProvider

// App.js
import { AuthProvider } from './contexts/AuthContext';
import Navbar from './components/Navbar';

function App() {
  return (
    <AuthProvider>
      <h1>Mi Aplicación</h1>
      <Navbar />
      {/* ...el resto de la app... */}
    </AuthProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Usar el contexto en cualquier componente

// components/Navbar.js
import { useAuth } from '../contexts/AuthContext';

function Navbar() {
  const { user, login, logout } = useAuth(); // ¡Así de fácil!

  return (
    <nav>
      {user ? (
        <>
          <span>Bienvenido, {user.name}</span>
          <button onClick={logout}>Cerrar Sesión</button>
        </>
      ) : (
        <button onClick={() => login('UsuarioPrueba')}>Iniciar Sesión</button>
      )}
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

✅ Buenas practicas y patrones comunes

  1. Crear un Custom Provider: Como en el ejemplo anterior, es una excelente práctica crear un componente MiProveedor que encapsule la lógica del estado (useState o useReducer) junto con el Context.Provider. Esto mantiene la lógica centralizada y el componente App más limpio.
  2. Crear un Custom Hook consumidor: useMiContexto (como useAuth) hace que el consumo sea más declarativo y permite añadir validaciones, como comprobar si el hook se está usando dentro del proveedor correcto.
  3. Separar contextos: No pongas todo el estado de tu aplicación en un solo contexto gigante. Divídelo por dominios: AuthContext, ThemeContext, CartContext, etc. Esto evita re-renderizados innecesarios.
  4. Optimización de value: Cada vez que el componente que contiene al Provider se re-renderiza, se crea un nuevo objeto para value. Si este objeto contiene funciones, créalas con useCallback para evitar re-renderizados innecesarios en los componentes consumidores.

🚨 Errores comunes y cómo evitarlos

  • Error: Olvidar envolver los componentes con el <MiContext.Provider>.
    • Solución: Asegúrate de que el Provider esté en un nivel superior en el árbol de componentes. El custom hook con la validación (como en el ejemplo de useAuth) te ayudará a detectar este error rápidamente.
  • Error: Re-renderizados excesivos.

    // PROBLEMA 😬
    function App() {
      const [count, setCount] = useState(0);
      // El objeto `value` se crea de nuevo en CADA render de App,
      // incluso si `count` es el que cambia. Esto hace que TODOS los
      // consumidores de ThemeContext se re-rendericen.
      return (
        <ThemeContext.Provider value={{ theme: 'dark' }}>
           <button onClick={() => setCount(c => c + 1)}>{count}</button>
           <MiComponentePesado />
        </ThemeContext.Provider>
      );
    }
    
    • Solución: Saca el value del flujo del render si es estático, o memorízalo con useMemo si depende de props o estado.
    // BIEN ✅
    const themeValue = useMemo(() => ({ theme: 'dark' }), []);
    // O mejor aún, si es totalmente estático:
    const themeValue = { theme: 'dark' };
    return <ThemeContext.Provider value={themeValue}>...</ThemeContext.Provider>;
    

🚀 Retos prácticos

  1. Selector de Idioma: Crea un LanguageContext que permita a los componentes mostrar texto en "Español" o "Inglés". Unos botones en la Navbar deberían poder cambiar el idioma de toda la aplicación.
  2. Notificaciones Globales: Implementa un NotificationContext que permita a cualquier componente mostrar un mensaje de notificación (ej. "Producto añadido al carrito"). Debe tener una función showNotification(message).
  3. Carrito de Compras con useReducer y useContext: Combina ambos hooks. Crea un CartContext que use un useReducer internamente para manejar la lógica de añadir, eliminar y actualizar productos en el carrito.

Top comments (0)