Skip to content

Context API ​

Cuando muchos componentes en distintos niveles necesitan el mismo dato (el usuario con sesión, el tema, el idioma, el carrito), pasarlo por props de nivel en nivel se vuelve tedioso. Context permite que cualquier componente lea ese dato directamente. Aprenderás a crearlo, combinarlo con useReducer y sus límites.

Concepto ​

El problema: prop drilling ​

txt
<App usuario={usuario}>
  <Layout usuario={usuario}>          ← no lo usa, sólo lo pasa
    <Header usuario={usuario}>        ← no lo usa, sólo lo pasa
      <MenuUsuario usuario={usuario}> ← ¡por fin lo usa!

Pasar props por componentes que no las necesitan se llama prop drilling ("perforar" el árbol). Funciona, pero es frágil y molesto.

La solución: Context ​

Context es como una estación de radio: el Provider transmite un valor y cualquier componente dentro de él puede "sintonizarlo" con useContext, sin importar qué tan profundo esté.

txt
<AuthProvider value={usuario}>    📡 transmite
  <Layout>
    <Header>
      <MenuUsuario />              📻 useContext(AuthContext) → usuario

Los tres pasos ​

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

// 1. Crear el contexto (con un valor por defecto si no hay Provider)
const TemaContext = createContext('claro')

// 2. Proveer el valor a un árbol de componentes
export default function App() {
  const [tema, setTema] = useState('claro')
  return (
    <TemaContext value={tema}>
      <button onClick={() => setTema((t) => (t === 'claro' ? 'oscuro' : 'claro'))}>Cambiar tema</button>
      <Pagina />
    </TemaContext>
  )
}

function Pagina() {
  return <Tarjeta /> // no necesita saber nada del tema
}

// 3. Consumir el valor donde se necesite
function Tarjeta() {
  const tema = useContext(TemaContext)
  return (
    <div style={{ background: tema === 'oscuro' ? '#1e293b' : '#f1f5f9', padding: 24 }}>
      Tema actual: {tema}
    </div>
  )
}

React 19

Desde React 19 puedes usar el contexto directamente como proveedor: <TemaContext value={...}>. En código anterior verás <TemaContext.Provider value={...}>, que sigue funcionando.

Patrón profesional: Provider + hook propio ​

En proyectos reales se encapsula todo en un archivo: el contexto, el componente Provider con su lógica y un hook para consumirlo.

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

const AuthContext = createContext(null)

// Simula una API de login
async function loginApi(email, password) {
  await new Promise((r) => setTimeout(r, 600))
  if (password !== '123456') throw new Error('Credenciales incorrectas')
  return { id: 1, nombre: email.split('@')[0], email, rol: 'admin' }
}

export function AuthProvider({ children }) {
  const [usuario, setUsuario] = useState(() => {
    const guardado = localStorage.getItem('usuario')
    return guardado ? JSON.parse(guardado) : null
  })

  async function login(email, password) {
    const u = await loginApi(email, password)
    setUsuario(u)
    localStorage.setItem('usuario', JSON.stringify(u))
  }

  function logout() {
    setUsuario(null)
    localStorage.removeItem('usuario')
  }

  const valor = { usuario, estaAutenticado: Boolean(usuario), login, logout }

  return <AuthContext value={valor}>{children}</AuthContext>
}

// Hook propio: más cómodo y con un error claro si se usa fuera del Provider
export function useAuth() {
  const contexto = useContext(AuthContext)
  if (!contexto) throw new Error('useAuth debe usarse dentro de <AuthProvider>')
  return contexto
}
jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { AuthProvider } from './context/AuthContext'
import App from './App'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <AuthProvider>
      <App />
    </AuthProvider>
  </StrictMode>
)
jsx
import { useState } from 'react'
import { useAuth } from './context/AuthContext'

function Header() {
  const { usuario, estaAutenticado, logout } = useAuth()
  return (
    <header style={{ display: 'flex', justifyContent: 'space-between', padding: 16, borderBottom: '1px solid #ddd' }}>
      <strong>Mi App</strong>
      {estaAutenticado ? (
        <span>
          Hola, {usuario.nombre} <button onClick={logout}>Salir</button>
        </span>
      ) : (
        <span>Invitado</span>
      )}
    </header>
  )
}

function Login() {
  const { login } = useAuth()
  const [error, setError] = useState('')
  const [cargando, setCargando] = useState(false)

  async function enviar(e) {
    e.preventDefault()
    const datos = new FormData(e.currentTarget)
    setCargando(true)
    setError('')
    try {
      await login(datos.get('email'), datos.get('password'))
    } catch (err) {
      setError(err.message)
    } finally {
      setCargando(false)
    }
  }

  return (
    <form onSubmit={enviar} style={{ display: 'grid', gap: 8, maxWidth: 300, margin: '2rem auto' }}>
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Contraseña (123456)" required />
      <button disabled={cargando}>{cargando ? 'Entrando…' : 'Entrar'}</button>
      {error && <p style={{ color: 'crimson' }}>{error}</p>}
    </form>
  )
}

function Panel() {
  const { usuario } = useAuth()
  return <main style={{ padding: 16 }}>Bienvenido al panel, rol: {usuario.rol}</main>
}

export default function App() {
  const { estaAutenticado } = useAuth()
  return (
    <>
      <Header />
      {estaAutenticado ? <Panel /> : <Login />}
    </>
  )
}

Context + useReducer: un carrito global ​

Cuando la lógica de actualización es compleja (muchas acciones distintas), useReducer la organiza en una sola función pura:

jsx
const [estado, dispatch] = useReducer(reducer, estadoInicial)
dispatch({ type: 'agregar', producto }) // "despachar" una acción

El reducer recibe el estado actual y una acción, y devuelve el nuevo estado. Es como un empleado de almacén con un manual: según el tipo de orden ("agregar", "quitar", "vaciar"), sabe exactamente qué hacer.

jsx
import { createContext, useContext, useReducer } from 'react'

const CarritoContext = createContext(null)

function carritoReducer(estado, accion) {
  switch (accion.type) {
    case 'agregar': {
      const existe = estado.items.find((i) => i.id === accion.producto.id)
      const items = existe
        ? estado.items.map((i) => (i.id === accion.producto.id ? { ...i, cantidad: i.cantidad + 1 } : i))
        : [...estado.items, { ...accion.producto, cantidad: 1 }]
      return { ...estado, items }
    }
    case 'cambiarCantidad':
      return {
        ...estado,
        items: estado.items
          .map((i) => (i.id === accion.id ? { ...i, cantidad: accion.cantidad } : i))
          .filter((i) => i.cantidad > 0)
      }
    case 'quitar':
      return { ...estado, items: estado.items.filter((i) => i.id !== accion.id) }
    case 'vaciar':
      return { ...estado, items: [] }
    default:
      throw new Error(`Acción desconocida: ${accion.type}`)
  }
}

export function CarritoProvider({ children }) {
  const [estado, dispatch] = useReducer(carritoReducer, { items: [] })

  // Valores derivados
  const totalPiezas = estado.items.reduce((s, i) => s + i.cantidad, 0)
  const total = estado.items.reduce((s, i) => s + i.precio * i.cantidad, 0)

  const valor = {
    items: estado.items,
    totalPiezas,
    total,
    agregar: (producto) => dispatch({ type: 'agregar', producto }),
    cambiarCantidad: (id, cantidad) => dispatch({ type: 'cambiarCantidad', id, cantidad }),
    quitar: (id) => dispatch({ type: 'quitar', id }),
    vaciar: () => dispatch({ type: 'vaciar' })
  }

  return <CarritoContext value={valor}>{children}</CarritoContext>
}

export function useCarrito() {
  const ctx = useContext(CarritoContext)
  if (!ctx) throw new Error('useCarrito debe usarse dentro de <CarritoProvider>')
  return ctx
}
jsx
import { CarritoProvider, useCarrito } from './context/CarritoContext'

const PRODUCTOS = [
  { id: 1, nombre: 'Café de Chiapas', precio: 180 },
  { id: 2, nombre: 'Taza de barro', precio: 120 },
  { id: 3, nombre: 'Prensa francesa', precio: 450 }
]

function IconoCarrito() {
  const { totalPiezas } = useCarrito()
  return <span>🛒 {totalPiezas}</span>
}

function Catalogo() {
  const { agregar } = useCarrito()
  return PRODUCTOS.map((p) => (
    <div key={p.id}>
      {p.nombre} — ${p.precio} <button onClick={() => agregar(p)}>Agregar</button>
    </div>
  ))
}

function Resumen() {
  const { items, total, cambiarCantidad, vaciar } = useCarrito()
  if (items.length === 0) return <p>Carrito vacío</p>
  return (
    <div>
      {items.map((i) => (
        <div key={i.id}>
          {i.nombre}
          <input
            type="number"
            min={0}
            value={i.cantidad}
            onChange={(e) => cambiarCantidad(i.id, Number(e.target.value))}
            style={{ width: 60, marginLeft: 8 }}
          />
        </div>
      ))}
      <strong>Total: ${total}</strong> <button onClick={vaciar}>Vaciar</button>
    </div>
  )
}

export default function App() {
  return (
    <CarritoProvider>
      <header><IconoCarrito /></header>
      <Catalogo />
      <hr />
      <Resumen />
    </CarritoProvider>
  )
}

Límites y rendimiento ​

Cuando el valor de un contexto cambia, todos los componentes que lo consumen se vuelven a renderizar. Consejos:

  • Divide contextos por responsabilidad: AuthContext, TemaContext, CarritoContext. No metas todo en un AppContext gigante.
  • Si un contexto cambia muy seguido (posición del mouse, texto de un input), no es buena opción.
  • Para estado global complejo o que cambia mucho, usa Zustand (lección 15): sólo re-renderiza los componentes que usan la parte que cambió.
  • Para datos del servidor (productos, pedidos), usa TanStack Query, no Context.
UsoHerramienta
Tema, idioma, usuario autenticado✅ Context
Carrito, preferencias, UI globalContext + useReducer o Zustand
Datos de la APITanStack Query
Estado de un solo componenteuseState

Buena práctica

Siempre crea un hook propio (useAuth, useCarrito) que lance un error si se usa fuera del Provider. Te ahorrará horas de depurar valores undefined.

Error común

Olvidar envolver la app con el Provider. useContext devuelve el valor por defecto de createContext (a menudo null) y obtienes errores como "Cannot read properties of null".

Ejercicios ​

  1. Crea un IdiomaContext con es y en, un hook useIdioma() que devuelva { idioma, setIdioma, t }, donde t('saludo') traduce claves desde un objeto de traducciones.
  2. Agrega al CarritoContext la acción aplicarCupon que guarde un cupón en el estado; si es DESC10, el total tiene 10 % de descuento.
Ver solución
jsx
// 1. src/context/IdiomaContext.jsx
import { createContext, useContext, useState } from 'react'

const TRADUCCIONES = {
  es: { saludo: '¡Hola!', carrito: 'Carrito', comprar: 'Comprar' },
  en: { saludo: 'Hello!', carrito: 'Cart', comprar: 'Buy' }
}

const IdiomaContext = createContext(null)

export function IdiomaProvider({ children }) {
  const [idioma, setIdioma] = useState('es')
  const t = (clave) => TRADUCCIONES[idioma][clave] ?? clave
  return <IdiomaContext value={{ idioma, setIdioma, t }}>{children}</IdiomaContext>
}

export function useIdioma() {
  const ctx = useContext(IdiomaContext)
  if (!ctx) throw new Error('useIdioma debe usarse dentro de <IdiomaProvider>')
  return ctx
}

// Uso
// const { t, idioma, setIdioma } = useIdioma()
// <h1>{t('saludo')}</h1>
// <button onClick={() => setIdioma(idioma === 'es' ? 'en' : 'es')}>ES/EN</button>
jsx
// 2. En carritoReducer agrega:
case 'aplicarCupon':
  return { ...estado, cupon: accion.cupon.trim().toUpperCase() }

// Estado inicial: { items: [], cupon: null }

// En CarritoProvider:
const subtotal = estado.items.reduce((s, i) => s + i.precio * i.cantidad, 0)
const descuento = estado.cupon === 'DESC10' ? subtotal * 0.1 : 0
const total = subtotal - descuento
// y en valor: subtotal, descuento, total, cupon: estado.cupon,
// aplicarCupon: (cupon) => dispatch({ type: 'aplicarCupon', cupon })

Resumen ​

  • Context evita el prop drilling: createContext → Provider con value → useContext.
  • Patrón profesional: archivo con contexto + Provider + hook propio con validación.
  • useReducer organiza lógica de estado compleja en una función pura con acciones.
  • Divide contextos por responsabilidad; para estado global grande usa Zustand y para datos del servidor TanStack Query.

Academia Web — aprende, copia y construye.