Estado global — Context + useReducer
En este capítulo aprenderás a usar React Context + useReducer para gestionar datos compartidos entre componentes y páginas, e implementar la función de «guardar artículos en favoritos».
¿Por qué se necesita un estado global?
Hasta ahora, los datos se pasan de nivel en nivel mediante Props. Pero algunos datos necesitan ser accedidos por múltiples componentes no relacionados:
- La lista de favoritos necesita compartirse entre la página de inicio, la página de detalles y la barra de navegación
- Todas las páginas necesitan conocer el estado de inicio de sesión del usuario
- Los datos del carrito de compras necesitan acceso entre páginas
Paso de Props por niveles (Prop Drilling)Su dolor de cabeza: A → B → C → D, los componentes intermedios se ven obligados a recibir Props que no usan.
ContextProporciona un «canal de datos» global, cualquier componente descendiente puede leer directamente, saltándose las capas intermedias.
Conceptos clave de Context
| Paso | Código | Función |
|---|---|---|
| Crear el Context | createContext() | Crea un contenedor de datos compartido |
| Proveer datos | <Provider value={...}> | Envuelve la parte superior del árbol de componentes e inyecta datos |
| Consumir datos | useContext(MyContext) | Leer datos en cualquier componente descendiente |
createContext y Provider
Ejemplo
// Primer paso: crear el Context
const ThemeContext = createContext(null)
// Segundo paso: crear el componente Provider
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light')
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light')
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
)
}
// Tercer paso: usar en cualquier componente
function ThemedButton() {
const { theme, toggleTheme } = useContext(ThemeContext)
return (
<button onClick={toggleTheme}>
Tema actual:{theme}
</button>
)
}
// Cuarto paso: envolver el Provider en App
function App() {
return (
<ThemeProvider>
<ThemedButton />
</ThemeProvider>
)
}
Context es como una «torre de radiodifusión»: el Provider transmite datos hacia afuera y useContext los recibe.
useReducer — Gestión de estados complejos
Cuando la lógica de estado se vuelve compleja (múltiples subestados, múltiples formas de actualización), useState se queda corto.
useReducerProporciona un patrón de gestión de estado similar a Redux:dispatch acción → el reducer calcula el nuevo estado。
Ejemplo
// Función reducer: (estado actual, acción) ⇒ nuevo estado
function favoriteReducer(state, action) {
switch (action.type) {
case 'TOGGLE':
// Si ya está en favoritos, lo quita; si no, lo añade
if (state.includes(action.id)) {
return state.filter(id => id !== action.id)
} else {
return [...state, action.id]
}
case 'CLEAR':
return []
default:
return state
}
}
function FavoriteDemo() {
// useReducer(reducer, valor inicial)
const [favoriteIds, dispatch] = useReducer(favoriteReducer, [])
return (
<div>
<p>Número de favoritos:{favoriteIds.length}</p>
<button onClick={() => dispatch({ type: 'TOGGLE', id: 1 })}>
{favoriteIds.includes(1) ? 'Quitar favorito 1' : 'Favorito 1'}
</button>
<button onClick={() => dispatch({ type: 'CLEAR' })}>Vaciar favoritos</button>
</div>
)
}
useState vs useReducer
| Escenario | Recomendación | Razón |
|---|---|---|
| Estado único e independiente | useState | Simple y directo |
| Múltiples estados interrelacionados | useReducer | Un solo dispatch puede actualizar múltiples estados |
| Lógica de actualización compleja | useReducer | El reducer tiene tipos de acción claros, fáciles de depurar |
| Necesita pasarse a componentes profundos | useState o useReducer | Ambos se pueden usar con Context |
Patrón de combinación Context + useReducer
Esta es la solución de gestión de estado global más potente en React, y también la idea central de Redux.
Ejemplo
import { createContext, useContext, useReducer, useEffect } from 'react'
// 1. Crear el Context
const FavoriteContext = createContext(null)
// 2. Definir el reducer
function favoriteReducer(state, action) {
switch (action.type) {
case 'TOGGLE':
if (state.includes(action.id)) {
return state.filter(id => id !== action.id)
} else {
return [...state, action.id]
}
case 'CLEAR':
return []
default:
return state
}
}
// 3. Componente Provider: combinar Context + useReducer
export function FavoriteProvider({ children }) {
// Restaurar el estado inicial desde localStorage
const [favoriteIds, dispatch] = useReducer(favoriteReducer, [], () => {
const saved = localStorage.getItem('blog-favorites')
return saved ? JSON.parse(saved) : []
})
// Cuando la lista de favoritos cambie, sincronizar automáticamente con localStorage
useEffect(() => {
localStorage.setItem('blog-favorites', JSON.stringify(favoriteIds))
}, [favoriteIds])
// Encapsular algunos métodos comunes
function toggleFavorite(id) {
dispatch({ type: 'TOGGLE', id })
}
function isFavorite(id) {
return favoriteIds.includes(id)
}
const value = {
favoriteIds,
favoriteCount: favoriteIds.length,
toggleFavorite,
isFavorite
}
return (
<FavoriteContext.Provider value={value}>
{children}
</FavoriteContext.Provider>
)
}
// 4. Hook personalizado: encapsular useContext para que la llamada sea más simple
export function useFavorites() {
const context = useContext(FavoriteContext)
if (!context) {
throw new Error('useFavorites debe usarse dentro de FavoriteProvider')
}
return context
}
Registrar el Provider en main.jsx
Ejemplo
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { FavoriteProvider } from './context/FavoriteContext'
import router from './router'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
{/* El Provider envuelve la capa más externa, todos los componentes pueden acceder */}
<FavoriteProvider>
<RouterProvider router={router} />
</FavoriteProvider>
</React.StrictMode>
)
¿Por qué colocar el Provider fuera del RouterProvider? Porque la barra de navegación NavBar (en App) también necesita leer el estado de favoritos. El Provider debe envolver todos los componentes que necesiten acceder a este estado.
Usar la función de favoritos en los componentes
Botón de favoritos en BlogCard
Ejemplo
import { Link } from 'react-router-dom'
import { useFavorites } from '../context/FavoriteContext'
function BlogCard({ id, title, summary, date, category }) {
const { isFavorite, toggleFavorite } = useFavorites()
function handleFavorite(e) {
e.preventDefault() // Evitar la navegación del Link
toggleFavorite(id)
}
return (
<Link to={`/post/${id}`} className="card-link">
<div className="card">
<span className="tag">{category}</span>
<h3>{title}</h3>
<p>{summary}</p>
<div className="card-footer">
<span className="date">{date}</span>
<button className="fav-btn" onClick={handleFavorite}>
{isFavorite(id) ? '♥' : '♡'}
</button>
</div>
</div>
</Link>
)
}
export default BlogCard
Botón de favoritos en PostPage
Ejemplo
import { useFavorites } from '../context/FavoriteContext'
function PostPage() {
// ... otro código
const { isFavorite, toggleFavorite } = useFavorites()
return (
<article className="post-view">
<div className="post-header">
<h1>{article.title}</h1>
<button className="fav-btn" onClick={() => toggleFavorite(article.id)}>
{isFavorite(article.id) ? '♥ En favoritos' : '♡ Favorito'}
</button>
</div>
{/* ... */}
</article>
)
}
Mostrar el número de favoritos en NavBar
Ejemplo
import { useFavorites } from '../context/FavoriteContext'
function NavBar() {
const { favoriteCount } = useFavorites()
return (
<header className="navbar">
<a href="/" className="logo">EJEMPLO Blog</a>
<nav>
<a href="/">Inicio</a>
{favoriteCount > 0 && (
<span className="fav-badge">Favoritos{favoriteCount}</span>
)}
</nav>
</header>
)
}
Después de guardar un artículo en favoritos desde la página de inicio, al entrar en la página de detalles se muestra automáticamente «En favoritos», y el número en la barra de navegación también se actualiza sincronizadamente: este es el valor del intercambio global de Context.
Context + useReducer vs Vue3 Pinia
| Característica | React Context + useReducer | Vue3 Pinia |
|---|---|---|
| Método de integración | Integrado en React, no requiere instalación | Librería independiente, requiere instalación |
| Definición del estado | Valor inicial de useReducer | state: () => ({}) |
| Actualización de estado | dispatch({ type, payload }) | Modificación directa o funciones de acción |
| Propiedades calculadas | useMemo manual | getters |
| Método reactivo | El cambio del value del Provider provoca un nuevo renderizado | Seguimiento automático de dependencias |
| DevTools | No integrado (requiere Redux DevTools) | Soporte nativo de Vue DevTools |
Resumen del capítulo
En este capítulo has dominado la solución completa de gestión de estado global en React: createContext crea el contenedor, Provider inyecta datos, useContext consume datos, useReducer gestiona la lógica de actualización compleja.
La función «guardar artículos en favoritos» mantiene los datos de la página de inicio, la página de detalles y la barra de navegación sincronizados globalmente, y tras actualizar la página también se pueden restaurar mediante localStorage.
Otras extensiones