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

PasoCódigoFunción
Crear el ContextcreateContext()Crea un contenedor de datos compartido
Proveer datos<Provider value={...}>Envuelve la parte superior del árbol de componentes e inyecta datos
Consumir datosuseContext(MyContext)Leer datos en cualquier componente descendiente

createContext y Provider

Ejemplo

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

// 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

import { useReducer } from 'react'

// 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

EscenarioRecomendaciónRazón
Estado único e independienteuseStateSimple y directo
Múltiples estados interrelacionadosuseReducerUn solo dispatch puede actualizar múltiples estados
Lógica de actualización complejauseReducerEl reducer tiene tipos de acción claros, fáciles de depurar
Necesita pasarse a componentes profundosuseState o useReducerAmbos 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

// Ruta del archivo: src/context/FavoriteContext.jsx
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

// Ruta del archivo: src/main.jsx
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

// Ruta del archivo: src/components/BlogCard.jsx
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) ? '&#x2665;' : '♡'}
          </button>
        </div>
      </div>
    </Link>
  )
}

export default BlogCard

Botón de favoritos en PostPage

Ejemplo

// Ruta del archivo: src/pages/PostPage.jsx (parte clave)
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) ? '&#x2665; En favoritos' : '♡ Favorito'}
        </button>
      </div>
      {/* ... */}
    </article>
  )
}

Mostrar el número de favoritos en NavBar

Ejemplo

// Ruta del archivo: src/components/NavBar.jsx
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ísticaReact Context + useReducerVue3 Pinia
Método de integraciónIntegrado en React, no requiere instalaciónLibrería independiente, requiere instalación
Definición del estadoValor inicial de useReducerstate: () => ({})
Actualización de estadodispatch({ type, payload })Modificación directa o funciones de acción
Propiedades calculadasuseMemo manualgetters
Método reactivoEl cambio del value del Provider provoca un nuevo renderizadoSeguimiento automático de dependencias
DevToolsNo 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