Redux es una biblioteca de gestión de estado para aplicaciones JavaScript y TypeScript. Resuelve un problema específico: a medida que las aplicaciones crecen, distintas partes del sistema necesitan compartir y modificar los mismos datos, y hacerlo a través del estado local de los componentes o de props profundamente anidadas se vuelve inmanejable. Redux centraliza esos datos en un único store e impone un ciclo de actualización estricto.

Por Qué Importa Redux

Sin una solución centralizada de estado, terminas con datos dispersos entre componentes, emisores de eventos disparando en orden impredecible y cadenas de props de tres o cuatro niveles de profundidad. Redux aborda esto haciendo que el flujo de datos sea explícito. Los cambios de estado solo ocurren mediante actions despachadas, y esos cambios son gestionados por funciones puras llamadas reducers.

El resultado es que las transiciones de estado son trazables, los bugs son reproducibles, y agregar nuevas funcionalidades no requiere desenredar la lógica de estado existente.

Principios Fundamentales de Redux

Redux está construido en torno a tres principios:

  1. Fuente Única de la Verdad: Todo el estado de la aplicación vive en un único objeto JavaScript, el store. Un solo lugar donde mirar al depurar.

  2. El Estado es Solo Lectura: La única forma de cambiar el estado es despachar una action, un objeto simple que describe lo que ocurrió. Esto evita que las actualizaciones aparezcan en lugares inesperados.

  3. Los Cambios se Realizan con Funciones Puras: Los reducers reciben el estado actual y una action, y devuelven el siguiente estado sin mutar el original. Las mismas entradas siempre producen la misma salida.

¿Cuándo Usar Redux?

Redux es una buena opción cuando múltiples componentes necesitan acceder a los mismos datos, cuando necesitas depuración con viaje en el tiempo o registro de auditoría de cambios de estado, o cuando la aplicación es lo suficientemente compleja como para que el estado impredecible se convierta en un problema real de mantenimiento.

Para aplicaciones pequeñas, Redux suele ser excesivo. El estado local de los componentes y React Context cubren la mayoría de los casos a esa escala. El costo en boilerplate de Redux se justifica cuando la lógica de estado es genuinamente compleja.

Primeros Pasos con Redux

1. Instala Redux y las Bibliotecas de Soporte

npm install redux react-redux

Con TypeScript:

npm install --save-dev @types/redux @types/react-redux

2. Crea tus Actions

Las actions son objetos simples con un campo type y datos opcionales:

// actions.ts
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export function increment() {
  return { type: INCREMENT };
}

export function decrement() {
  return { type: DECREMENT };
}

3. Define un Reducer

El reducer es una función pura que devuelve un nuevo objeto de estado sin mutar el original:

// reducer.ts
import { INCREMENT, DECREMENT } from "./actions";

interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export function counterReducer(
  state: CounterState = initialState,
  action: { type: string }
): CounterState {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

4. Crea el Store

// store.ts
import { createStore } from "redux";
import { counterReducer } from "./reducer";

export const store = createStore(counterReducer);

5. Integra con React

El componente Provider pone el store a disposición de cualquier componente anidado:

// index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App";

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Un Ejemplo Completo

Aquí hay un ejemplo funcional en TypeScript que cubre actions, reducer, store y un componente React conectado.

Paso 1: Estructura del Proyecto

src
├── components
│   └── Counter.tsx
├── redux
│   ├── actions.ts
│   ├── reducer.ts
│   └── store.ts
└── App.tsx

Paso 2: Crea los Tipos de Action y los Action Creators

// src/redux/actions.ts
export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

interface IncrementAction {
  type: typeof INCREMENT;
}

interface DecrementAction {
  type: typeof DECREMENT;
}

export type CounterActionTypes = IncrementAction | DecrementAction;

export function increment(): CounterActionTypes {
  return { type: INCREMENT };
}

export function decrement(): CounterActionTypes {
  return { type: DECREMENT };
}

Las interfaces de TypeScript para cada tipo de action garantizan que el código permanezca con tipado seguro en todo el reducer y en cualquier componente que despache actions.

Paso 3: Define el Reducer

// src/redux/reducer.ts
import { INCREMENT, DECREMENT, CounterActionTypes } from "./actions";

export interface CounterState {
  value: number;
}

const initialState: CounterState = {
  value: 0,
};

export function counterReducer(
  state = initialState,
  action: CounterActionTypes
): CounterState {
  switch (action.type) {
    case INCREMENT:
      return { ...state, value: state.value + 1 };
    case DECREMENT:
      return { ...state, value: state.value - 1 };
    default:
      return state;
  }
}

El counterReducer impone un único punto de modificación del estado: solo esta función cambia el estado, y lo hace devolviendo un nuevo objeto.

Paso 4: Crea el Store

// src/redux/store.ts
import { createStore } from "redux";
import { counterReducer } from "./reducer";

export const store = createStore(counterReducer);

Para aplicaciones más complejas, applyMiddleware añade middlewares como Redux Thunk o Redux Saga para gestionar operaciones asíncronas.

Paso 5: Construye un Componente Counter

// src/components/Counter.tsx
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "../redux/actions";
import { CounterState } from "../redux/reducer";

export function Counter() {
  const dispatch = useDispatch();
  const value = useSelector((state: CounterState) => state.value);

  return (
    <div>
      <h1>Redux Counter</h1>
      <p>Value: {value}</p>
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
}

useSelector recupera el valor actual del contador desde el store. useDispatch envía las actions.

Paso 6: Conecta Todo en la App

// src/App.tsx
import React from "react";
import { Counter } from "./components/Counter";

function App() {
  return <Counter />;
}

export default App;

Ejecutando la Aplicación

npm install
npm start

Abre un navegador y verás el contador. Los botones de incrementar y decrementar actualizan el estado a través de Redux, con cada cambio registrado en el store.

Buenas Prácticas para Escalar con Redux

  1. Usa Middleware para Efectos Secundarios: Para operaciones asíncronas como llamadas a APIs, añade redux-thunk o redux-saga. Estos proporcionan patrones estructurados para los efectos secundarios en lugar de despachar actions desde dentro de los componentes.

  2. Normaliza el Estado para Entidades Complejas: Si almacenas arrays de elementos o datos profundamente anidados, normalízalos. Bibliotecas como normalizr aplanan estructuras complejas, haciendo las actualizaciones y búsquedas más eficientes.

  3. Mantén los Reducers Enfocados: Cada reducer debe gestionar una porción del estado. Un reducer que maneja usuarios, pedidos y notificaciones es un reducer que está haciendo demasiado.

  4. Combina Reducers: Usa combineReducers a medida que la aplicación crece. Cada reducer gestiona su porción, y combineReducers ensambla el árbol de estado completo.

  5. Aprovecha el DevTools: La extensión Redux DevTools proporciona depuración con viaje en el tiempo y visibilidad total sobre cada cambio de estado. Vale la pena configurarla desde el primer día.

  6. Usa TypeScript de Forma Efectiva: Las actions, reducers y selectors tipados previenen toda una clase de errores en tiempo de ejecución. La inversión inicial en tipado se amortiza rápidamente en lógicas de estado complejas.

Errores Comunes y Cómo Evitarlos

  1. Abusar de Redux: No todo el estado pertenece a Redux. Las flags específicas de UI como el estado abierto/cerrado de un modal, estados de hover o valores de formularios locales generalmente es mejor mantenerlos en el estado del componente. Redux es para datos que múltiples componentes comparten.

  2. Mutar el Estado en los Reducers: La mutación directa provoca bugs sutiles. Siempre devuelve un nuevo objeto de estado usando operadores spread. Evita state.value++ o cualquier reasignación directa.

  3. Ignorar el Rendimiento: Los re-renders excesivos degradan el rendimiento en aplicaciones grandes. Los selectors memoizados de bibliotecas como Reselect evitan re-renders irrelevantes al computar datos derivados de forma eficiente.

  4. Descuidar la Organización del Código: A medida que la base de código crece, mantén las actions, reducers y tipos claramente separados. Una organización mezclada ralentiza a los nuevos colaboradores y dificulta la depuración.

Más Allá de los Fundamentos

Con los fundamentos sólidos, algunas herramientas amplían las capacidades de Redux:

  • Redux Observable gestiona flujos asíncronos complejos usando observables de RxJS.
  • Reselect crea selectors componibles y memoizados para datos derivados.
  • Redux Toolkit reduce el boilerplate significativamente y es ahora el enfoque recomendado para nuevos proyectos Redux.
  • Los tipos utilitarios de TypeScript como ReturnType y Extract simplifican las definiciones de tipo en configuraciones complejas de actions y reducers.

Reflexiones Finales sobre Redux

Redux sigue siendo una opción confiable para la gestión de estado predecible en grandes aplicaciones JavaScript y TypeScript. La verbosidad es real, y el costo del boilerplate es real, pero también lo es el retorno: los cambios de estado son trazables, los bugs son reproducibles, y la base de código se mantiene manejable a medida que crece.

Ya sea que estés comenzando un nuevo proyecto o refactorizando uno existente, la estructura de Redux recompensa la disciplina. El DevTools por sí solo ha ahorrado más horas de depuración que el costo de configuración en la mayoría de los proyectos en los que he trabajado.