DEV Community

Pedro Alvarado
Pedro Alvarado

Posted on

🚀 Gestión de estado en React

Esta guía explora dos patrones poderosos para manejar el estado en aplicaciones React que superan las capacidades de un simple useState.

  1. El Patrón nativo (useContext + useReducer): La solución integrada de React para manejar estado complejo y compartirlo a través de la aplicación sin necesidad de librerías externas. Ideal para aplicaciones de tamaño pequeño a mediano.
  2. Redux con Hooks (useSelector y useDispatch): La solución estándar de la industria para aplicaciones a gran escala que requieren un estado global predecible, herramientas de depuración avanzadas y un ecosistema robusto.

🔹 1. El patrón nativo: useContext + useReducer

Para muchas aplicaciones, no necesitas una librería externa como Redux. La combinación de useContext y useReducer te da un "mini-Redux" propio, directamente con las herramientas que provee React.

¿Cuándo es esta la solución correcta?

  • Cuando el estado es demasiado complejo para useState.
  • Cuando la lógica de actualización del estado es compartida por muchos componentes.
  • Cuando necesitas evitar el "prop drilling" (pasar props a través de muchos niveles).
  • Cuando quieres una solución ligera sin añadir dependencias a tu proyecto.

El patrónexplicado

La idea es simple:

  1. useReducer: Centraliza toda la lógica de estado en una función reducer.
  2. useContext: Provee el estado y la función dispatch a cualquier componente hijo que lo necesite, sin pasar props manualmente.

Ejemplo práctico: carrito de compras

Vamos a implementar un carrito de compras. La lógica (añadir, eliminar, etc.) vivirá en el reducer, y el estado del carrito estará disponible en toda la aplicación gracias al contexto.

Paso 1: Crear el CartContext.js

Este archivo contendrá toda nuestra lógica: el estado inicial, las acciones, el reducer y el proveedor del contexto.

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

// --- Estado Inicial y Acciones ---
const initialState = {
  items: [],
  total: 0,
};

export const CartActions = {
  ADD_ITEM: 'ADD_ITEM',
  REMOVE_ITEM: 'REMOVE_ITEM',
  CLEAR_CART: 'CLEAR_CART',
};

// --- El Reducer: El cerebro de nuestro carrito ---
function cartReducer(state, action) {
  switch (action.type) {
    case CartActions.ADD_ITEM: {
      const newItem = action.payload;
      const existingItem = state.items.find(item => item.id === newItem.id);

      let updatedItems;
      if (existingItem) {
        updatedItems = state.items.map(item =>
          item.id === newItem.id ? { ...item, quantity: item.quantity + 1 } : item
        );
      } else {
        updatedItems = [...state.items, { ...newItem, quantity: 1 }];
      }

      return {
        ...state,
        items: updatedItems,
        total: state.total + newItem.price,
      };
    }
    case CartActions.REMOVE_ITEM: {
      const itemToRemove = state.items.find(item => item.id === action.payload.id);
      if (!itemToRemove) return state;

      const updatedItems = state.items.filter(item => item.id !== itemToRemove.id);
      return {
        ...state,
        items: updatedItems,
        total: state.total - (itemToRemove.price * itemToRemove.quantity),
      };
    }
    case CartActions.CLEAR_CART:
      return initialState;
    default:
      return state;
  }
}

// --- Creación del Contexto y el Proveedor ---
const CartContext = createContext();

export function CartProvider({ children }) {
  const [state, dispatch] = useReducer(cartReducer, initialState);

  // El `value` del proveedor incluye tanto el estado como la función dispatch
  const value = { state, dispatch };

  return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
}

// --- Hook Personalizado para Consumir el Contexto ---
export function useCart() {
  const context = useContext(CartContext);
  if (context === undefined) {
    throw new Error('useCart debe ser usado dentro de un CartProvider');
  }
  return context;
}
Enter fullscreen mode Exit fullscreen mode

Paso 2: Envolver la aplicación con el CartProvider

En tu archivo principal (App.js o similar), envuelve los componentes que necesitan acceso al carrito.

// App.js
import { CartProvider } from './contexts/CartContext';
import ProductList from './components/ProductList';
import CartSummary from './components/CartSummary';

function App() {
  return (
    <CartProvider>
      <h1>Mi Tienda</h1>
      <ProductList />
      <CartSummary />
    </CartProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Paso 3: Usar el hook useCart en los componentes

Ahora, cualquier componente puede acceder y manipular el estado del carrito de forma muy limpia.

// components/ProductList.js
import { useCart, CartActions } from '../contexts/CartContext';

const products = [
  { id: 1, name: 'Laptop', price: 1200 },
  { id: 2, name: 'Teclado', price: 80 },
];

function ProductList() {
  const { dispatch } = useCart();

  const handleAddToCart = (product) => {
    dispatch({ type: CartActions.ADD_ITEM, payload: product });
  };

  return (
    <div>
      <h2>Productos</h2>
      {products.map(p => (
        <div key={p.id}>
          {p.name} - ${p.price}
          <button onClick={() => handleAddToCart(p)}>Añadir al carrito</button>
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// components/CartSummary.js
import { useCart } from '../contexts/CartContext';

function CartSummary() {
  const { state } = useCart(); // Solo necesitamos el estado aquí

  return (
    <div>
      <h2>Resumen del Carrito</h2>
      <p>Items: {state.items.length}</p>
      <p>Total: ${state.total.toFixed(2)}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Ventajas de este patrón

  • Sin dependencias externas: Usa solo APIs de React.
  • Lógica centralizada: El reducer es el único lugar donde se modifica el estado, haciéndolo predecible.
  • Código limpio: Los componentes son simples, solo despachan acciones o leen el estado.

Desventajas a considerar

  • Optimización de Renders: Por defecto, cualquier cambio en el contexto (incluso en una parte del estado que un componente no usa) hará que todos los componentes que consumen el contexto se re-rendericen. Esto puede ser un problema de rendimiento en apps grandes y requiere optimizaciones manuales con useMemo o separando contextos.
  • Sin DevTools: No tienes herramientas avanzadas de depuración como el Redux DevTools para inspeccionar el historial de acciones y estados.

🔹 2. Redux con hooks: useSelector y useDispatch

Redux es la librería más popular para gestionar el estado en aplicaciones JavaScript grandes. Aunque antes era conocido por su "boilerplate" (código repetitivo), Redux Toolkit (RTK) lo ha modernizado enormemente, haciéndolo mucho más simple y directo.

¿Cuándo dar el salto a Redux?

  • Cuando el estado de tu aplicación es grande, complejo y compartido por muchas partes de la UI.
  • Cuando necesitas una lógica de "middleware" para manejar efectos secundarios como llamadas a API de forma organizada (ej. Redux Thunk, Saga).
  • Cuando la depuración se vuelve difícil y necesitas herramientas avanzadas para viajar en el tiempo, inspeccionar cada acción y ver cómo cambia el estado (Redux DevTools).
  • Cuando el rendimiento se convierte en un problema y las optimizaciones de useContext no son suficientes. Redux está altamente optimizado para evitar re-renderizados innecesarios.

Ejemplo páctico: Carrito de Compras con Redux Toolkit

Vamos a reconstruir el mismo carrito de compras, pero esta vez con la potencia de Redux Toolkit.

Paso 1: Instalar las dependencias

npm install @reduxjs/toolkit react-redux
Enter fullscreen mode Exit fullscreen mode

Paso 2: Crear un "Slice" del Carrito

En Redux Toolkit, un "slice" es una porción del estado de Redux que contiene la lógica del reducer y las acciones para ese estado específico. ¡Todo en un solo archivo!

// store/cartSlice.js
import { createSlice } from '@reduxjs/toolkit';

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

const cartSlice = createSlice({
  name: 'cart',
  initialState,
  // Los "reducers" aquí son como los "cases" del switch.
  // RTK usa Immer por debajo, así que puedes "mutar" el estado de forma segura.
  reducers: {
    addItem(state, action) {
      const newItem = action.payload;
      const existingItem = state.items.find(item => item.id === newItem.id);

      if (existingItem) {
        existingItem.quantity++;
      } else {
        state.items.push({ ...newItem, quantity: 1 });
      }
      state.total += newItem.price;
    },
    removeItem(state, action) {
      const idToRemove = action.payload.id;
      const itemToRemove = state.items.find(item => item.id === idToRemove);

      if (itemToRemove) {
        state.total -= itemToRemove.price * itemToRemove.quantity;
        state.items = state.items.filter(item => item.id !== idToRemove);
      }
    },
    clearCart(state) {
      state.items = [];
      state.total = 0;
    },
  },
});

// RTK genera automáticamente las acciones a partir de tus reducers
export const { addItem, removeItem, clearCart } = cartSlice.actions;

// Exportamos el reducer para añadirlo al store
export default cartSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Paso 3: Configurar el Store de Redux

El "store" es el objeto global que contiene todo el estado de tu aplicación.

// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import cartReducer from './cartSlice';

export const store = configureStore({
  reducer: {
    cart: cartReducer,
    // Aquí puedes añadir otros slices, como: user: userReducer
  },
});
Enter fullscreen mode Exit fullscreen mode

Paso 4: Proveer el Store a la Aplicación

Similar a Context, Redux tiene un Provider que debe envolver tu aplicación.

// index.js (o App.js)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Paso 5: Usar useSelector y useDispatch en los Componentes

Ahora, refactorizamos nuestros componentes para usar los hooks de Redux.

  • useSelector(selectorFn): Lee datos del store. La función selector recibe el estado completo y devuelve la parte que te interesa.
  • useDispatch(): Devuelve la función dispatch del store para que puedas enviar acciones.
// components/ProductList.js
import { useDispatch } from 'react-redux';
import { addItem } from '../store/cartSlice'; // Importamos la acción

const products = [
  { id: 1, name: 'Laptop', price: 1200 },
  { id: 2, name: 'Teclado', price: 80 },
];

function ProductList() {
  const dispatch = useDispatch(); // Obtenemos la función dispatch

  const handleAddToCart = (product) => {
    // Despachamos la acción `addItem` con el producto como payload
    dispatch(addItem(product));
  };

  return (
    <div>
      <h2>Productos</h2>
      {products.map(p => (
        <div key={p.id}>
          {p.name} - ${p.price}
          <button onClick={() => handleAddToCart(p)}>Añadir al carrito</button>
        </div>
      ))}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
// components/CartSummary.js
import { useSelector } from 'react-redux';

function CartSummary() {
  // `useSelector` se suscribe a los cambios del store.
  // El componente se re-renderizará solo si los valores devueltos cambian.
  const cartItems = useSelector(state => state.cart.items);
  const cartTotal = useSelector(state => state.cart.total);

  return (
    <div>
      <h2>Resumen del Carrito</h2>
      <p>Items: {cartItems.length}</p>
      <p>Total: ${cartTotal.toFixed(2)}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🔹 3. Tabla comparativa: useContext/Reducer vs. Redux

Característica Patrón nativo (useContext + useReducer) Redux Toolkit con Hooks
Dependencias 0 (integrado en React) @reduxjs/toolkit, react-redux
Ideal para Apps pequeñas a medianas, estado "localizado" Apps medianas a grandes, estado global complejo
Rendimiento Bueno, pero requiere optimización manual (useMemo) para evitar renders innecesarios. Altamente optimizado. Los componentes solo se re-renderizan si la porción de datos que seleccionan cambia.
DevTools No disponibles Redux DevTools: depuración avanzada, viaje en el tiempo, inspección de acciones.
Middleware No soportado de forma nativa. Soportado (Thunk viene por defecto). Excelente para lógica asíncrona.
Boilerplate Moderado (crear contexto, reducer, proveedor). Mínimo con Redux Toolkit, pero más archivos y conceptos que aprender.
Curva de Aprendizaje Fácil si ya conoces los hooks de React. Moderada. Requiere entender slices, actions, store.

Conclusión: ¿cuál elegir?

La recomendación moderna es:

  1. Empieza siempre con las herramientas de React: Comienza con useState. Si el estado se vuelve complejo, refactoriza a useReducer. Si necesitas compartir ese estado, combínalo con useContext.
  2. Adopta Redux cuando sientas la necesidad: No empieces un proyecto con Redux "por si acaso". Introdúcelo cuando te enfrentes a los problemas que Redux resuelve brillantemente: depuración de estado compleja, manejo avanzado de efectos secundarios o cuellos de botella de rendimiento relacionados con useContext.

Top comments (0)