Gestión de estado con Context API y hooks

Lectura
25 min~6 min lectura

Concepto clave

La Context API de React es un sistema de gestión de estado diseñado para compartir datos entre componentes sin necesidad de pasar props manualmente a través de múltiples niveles. Imagina que estás construyendo una app de e-commerce: el carrito de compras necesita ser accesible desde la pantalla de productos, el header y la pantalla de checkout. Sin Context, tendrías que pasar el estado del carrito como prop desde el componente raíz hasta cada hijo, creando un "prop drilling" difícil de mantener.

La combinación con hooks como useContext y useReducer transforma esta API en una solución poderosa y moderna. Piensa en Context como un sistema de tuberías centralizado: en lugar de llevar agua (datos) manualmente a cada habitación (componente), instalas tuberías principales que todos pueden usar. Esto es especialmente valioso en React Native, donde la navegación entre pantallas y la sincronización de estado son críticas para la experiencia del usuario.

Cómo funciona en la práctica

Implementar Context API sigue un patrón claro. Primero, creas un contexto con React.createContext(). Luego, envuelves los componentes que necesitan acceso a ese contexto con un Provider, que actúa como el distribuidor de datos. Finalmente, los componentes hijos consumen el contexto usando el hook useContext.

Veamos el flujo paso a paso:

  1. Define el contexto y su valor inicial (ej: usuario autenticado, tema de la app).
  2. Crea un componente Provider que maneje el estado (con useState o useReducer) y lo exponga a través del contexto.
  3. Envuelve tu aplicación o la parte relevante con este Provider en el punto más alto necesario.
  4. En cualquier componente hijo, importa el contexto y usa useContext para acceder a los valores.

Este enfoque elimina la dependencia de props para datos globales, haciendo tu código más limpio y escalable cuando la app crece.

Codigo en accion

Antes: Pasando props manualmente (prop drilling)

// App.js
import React, { useState } from 'react';
import PantallaProductos from './PantallaProductos';
import Header from './Header';

export default function App() {
  const [carrito, setCarrito] = useState([]);
  
  return (
    <>
      <Header carrito={carrito} />
      <PantallaProductos carrito={carrito} setCarrito={setCarrito} />
    </>
  );
}

// PantallaProductos.js - y luego pasar a componentes más profundos...

Despues: Usando Context API y hooks

// CarritoContext.js
import React, { createContext, useReducer, useContext } from 'react';

const CarritoContext = createContext();

const initialState = {
  items: [],
  total: 0,
};

function carritoReducer(state, action) {
  switch (action.type) {
    case 'AGREGAR_ITEM':
      const nuevoItem = action.payload;
      return {
        items: [...state.items, nuevoItem],
        total: state.total + nuevoItem.precio,
      };
    case 'ELIMINAR_ITEM':
      const itemId = action.payload;
      const item = state.items.find(i => i.id === itemId);
      return {
        items: state.items.filter(i => i.id !== itemId),
        total: state.total - item.precio,
      };
    default:
      return state;
  }
}

export function CarritoProvider({ children }) {
  const [state, dispatch] = useReducer(carritoReducer, initialState);
  
  return (
    <CarritoContext.Provider value={{ state, dispatch }}>
      {children}
    </CarritoContext.Provider>
  );
}

export function useCarrito() {
  const context = useContext(CarritoContext);
  if (!context) {
    throw new Error('useCarrito debe usarse dentro de CarritoProvider');
  }
  return context;
}

// App.js
import React from 'react';
import { CarritoProvider } from './CarritoContext';
import PantallaProductos from './PantallaProductos';
import Header from './Header';

export default function App() {
  return (
    <CarritoProvider>
      <Header />
      <PantallaProductos />
    </CarritoProvider>
  );
}

// PantallaProductos.js
import React from 'react';
import { View, Text, Button } from 'react-native';
import { useCarrito } from './CarritoContext';

export default function PantallaProductos() {
  const { state, dispatch } = useCarrito();
  
  const agregarAlCarrito = (producto) => {
    dispatch({ type: 'AGREGAR_ITEM', payload: producto });
  };
  
  return (
    <View>
      <Text>Productos disponibles</Text>
      <Button 
        title="Agregar Producto A" 
        onPress={() => agregarAlCarrito({ id: 1, nombre: 'Producto A', precio: 10 })}
      />
      <Text>Items en carrito: {state.items.length}</Text>
    </View>
  );
}

Errores comunes

  • Provider no envuelve correctamente: Si usas useContext fuera del árbol del Provider, obtendrás el valor por defecto (a menudo undefined). Solución: Asegúrate de que el Provider esté en un nivel superior a todos los componentes que usen el contexto.
  • Re-renders innecesarios: Cambiar cualquier valor en el contexto hace que todos los componentes que usen useContext se re-rendericen, incluso si no usan esa parte del estado. Solución: Divide contextos por dominio (ej: CarritoContext, UsuarioContext) o usa memoización con React.memo.
  • Valor por defecto confuso: createContext() acepta un valor por defecto que solo se usa si no hay Provider. Si proporcionas un valor complejo aquí, puede enmascarar errores. Solución: Usa null o un valor simple, y maneja el error en un hook personalizado como useCarrito().
  • Abuso del contexto para estado local: No uses Context para estado que solo necesita un componente o unos pocos cercanos. Solución: Mantén el estado local con useState cuando sea apropiado; Context es para datos "globales".
  • Olvidar la optimización en listas: En listas largas, cada item que use contexto puede causar re-renders costosos. Solución: Considera pasar datos como props a componentes memoizados, o usa soluciones como Zustand para casos complejos.

Checklist de dominio

  1. Puedo crear un contexto con createContext y proporcionar valores iniciales.
  2. Sé envolver componentes con un Provider y manejar el estado usando useReducer.
  3. Puedo consumir contextos en componentes hijos con useContext sin errores.
  4. Reconozco cuándo usar Context API vs estado local o otras librerías como Redux.
  5. Implemento hooks personalizados (ej: useCarrito) para encapsular la lógica del contexto.
  6. Optimizo re-renders dividiendo contextos o usando React.memo cuando es necesario.
  7. Manejo errores como "context must be used within a Provider" en hooks personalizados.

Implementa un sistema de temas claro/oscuro con Context API

En esta práctica, crearás un sistema de temas para una app de React Native que permita cambiar entre modo claro y oscuro usando Context API y hooks. Sigue estos pasos:

  1. Crea un archivo TemaContext.js que defina un contexto para el tema. Usa createContext con un valor por defecto de 'claro'.
  2. Implementa un reducer o useState en un TemaProvider que maneje el estado del tema (puede ser 'claro' u 'oscuro').
  3. Expone una función para alternar el tema en el valor del Provider.
  4. Envuelve tu componente App con TemaProvider en App.js.
  5. Crea un componente ToggleTema.js que use useContext para acceder al contexto y muestre un botón que cambie el tema al presionarlo.
  6. Modifica los estilos de un componente existente (ej: una pantalla principal) para que respondan al tema actual, usando colores condicionales basados en el contexto.
  7. Prueba la funcionalidad ejecutando la app en Expo Go y verificando que el cambio de tema se refleje en la interfaz.
Pistas
  • Recuerda que el valor del contexto debe incluir tanto el estado actual como la función para cambiarlo.
  • Puedes usar el hook useColorScheme de react-native para detectar el tema del sistema como valor inicial.
  • Para estilos condicionales, considera usar un objeto que mapee temas a colores específicos.

Evalua tu comprension

Completa el quiz interactivo de arriba para ganar XP.