Tema
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) → usuarioLos 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ónEl 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 unAppContextgigante. - 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.
| Uso | Herramienta |
|---|---|
| Tema, idioma, usuario autenticado | ✅ Context |
| Carrito, preferencias, UI global | Context + useReducer o Zustand |
| Datos de la API | TanStack Query |
| Estado de un solo componente | useState |
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
- Crea un
IdiomaContextconesyen, un hookuseIdioma()que devuelva{ idioma, setIdioma, t }, dondet('saludo')traduce claves desde un objeto de traducciones. - Agrega al
CarritoContextla acciónaplicarCuponque guarde un cupón en el estado; si esDESC10, 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 convalue→useContext. - Patrón profesional: archivo con contexto + Provider + hook propio con validación.
useReducerorganiza 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.