Programación

¿Qué Redux y cómo funciona?

Por Antonio Richaud, Publicado el 10 de Enero de 2024

Introducción

En el desarrollo de aplicaciones web modernas, especialmente en entornos de desarrollo front-end, la gestión del estado se convierte en un desafío importante conforme las aplicaciones van creciendo en complejidad. Manejar el estado correctamente es esencial para garantizar que la interfaz de usuario (UI) responda de la mejor manera a las interacciones del usuario y refleje siempre la última información. Es aquí donde entra en juego Redux, una herramienta que se ha ganado el corazón de muchos desarrolladores por su enfoque estructurado para la gestión del estado.

Redux es una biblioteca de JavaScript que nos ayuda a gestionar el estado de las aplicaciones de manera centralizada y predecible. Fue creada por Dan Abramov y Andrew Clark en 2015, inspirada por la arquitectura Flux de Facebook, pero con un enfoque simplificado y más robusto. Aunque se usa normalmente con React, a Redux no le importa la biblioteca o framework que utilices, lo que significa que puedes integrarlo en cualquier proyecto de JavaScript.

La necesidad de Redux surge cuando empezamos a notar que el manejo del estado en componentes individuales se vuelve complicado de seguir y mantener. A medida que los datos fluyen a través de la aplicación, tener múltiples fuentes de verdad puede causar incoherencias, dificultando la depuración y la evolución del código. Lo que Redux propone es una solución sencilla: mantener todo el estado de la aplicación en un único objeto y actualizarlo de manera predecible mediante un flujo de datos unidireccional.

Acompáñame a platicar un poco sobre Redux en este pequeño artículo donde hablaremos a fondo sobre qué es Redux, cómo se estructura, y cómo puede ayudarte a manejar de manera eficiente el estado en tus aplicaciones. Desde los conceptos básicos hasta casos de uso avanzados, intentaré cubrir todo lo que necesitas saber para utilizar Redux en tu próximo desarrollo. :)

Conceptos básicos de Redux

Para entender Redux, es importante primero que nada familiarizarse con sus principios y conceptos más basicos. Redux se basa en tres principios fundamentales que guían su arquitectura y su funcionamiento.

1. Estado único

En Redux, todo el estado de la aplicación se almacena en un único objeto que se llama store. Este objeto contiene toda la información necesaria para la UI de la aplicación en un solo lugar, lo que facilita mucho el acceso y la gestión del estado. Tener un estado único también simplifica un monton la depuración, ya que puedes capturar el estado de la aplicación en cualquier momento.

2. El estado es de solo lectura

En Redux, el estado es inmutable, lo que significa que no se puede modificar directamente. La única manera de cambiar el estado es despachando una acción, que es un objeto que describe qué ocurrió en la aplicación. Este enfoque asegura que todos los cambios en el estado sean rastreables y predecibles.

3. Los cambios se realizan con funciones puras

Para especificar cómo cambia el estado en respuesta a una acción, Redux utiliza reducers, que son funciones puras. Una función pura es una función que, dada la misma entrada, siempre produce la misma salida sin causar efectos secundarios. Los reducers toman el estado anterior y la acción como argumentos, y retornan un nuevo estado.

Estos principios no solo hacen que Redux sea poderoso, sino que también proporcionan una estructura predecible y fácil de seguir, lo que facilita el mantenimiento y la escalabilidad de aplicaciones complejas.

Configuración e instalación

Ahora que ya entendimos los conceptos más básicos de Redux, ahora sí vamos a configurarlo en nuestro proyecto pero no te me preocupes porque yo te mostraré cómo instalar Redux e integrarlo con un proyecto sencillo de React.

Instalación de Redux

Para instalar Redux, puedes usar npm o yarn. Simplemente ejecutando uno de los siguientes comandos en tu terminal:


npm install redux react-redux

                    

yarn add redux react-redux

                    

Configuración del Store

Una vez que hayas instalado Redux, el siguiente paso es configurar el store. El store es el corazón de Redux, donde se almacena el estado global de la aplicación. Aquí tienes un ejemplo básico de cómo crear un store:


import { createStore } from 'redux';
import rootReducer from './reducers';
                    
const store = createStore(rootReducer);
                    
export default store;

                    

En este ejemplo, importamos createStore de Redux y unimos todos los reducers en un solo rootReducer. Luego, creamos el store con esta función y lo exportamos para que pueda ser utilizado en toda la aplicación.

Integración con React

Para conectar Redux con tu aplicación de React, necesitas utilizar el componente Provider de react-redux. Este componente hace que el store esté disponible para todos los componentes de la aplicación. Aquí tienes un ejemplo de cómo hacerlo:


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')
);

                    

En este código, envolvemos nuestro componente App con el componente Provider y le pasamos el store como prop. Esto asegura que cualquier componente dentro de App pueda acceder al estado global gestionado por Redux.

Con estos pasos, ya tendrás Redux configurado en tu proyecto React y listo para ser utilizado. En las siguientes secciones, exploraremos cómo manejar acciones, reducers, y otros aspectos avanzados de Redux.

Profundizando en Redux

Ahora que ya tienes Redux configurado, ahora sí vamonos reció y es importante entender cómo trabajar con las acciones y los reducers de manera más profunda. Estos son los componentes más importantes porque determinan cómo se actualiza el estado en respuesta a las interacciones del usuario.

Acciones y Reducers detallados

Las acciones en Redux son objetos que describen un evento que ocurre en la aplicación. Cada acción tiene al menos una propiedad type que indica el tipo de acción que se está realizando. A veces, las acciones también incluyen una payload con datos adicionales que pueden ser necesarios para actualizar el estado.


const addAction = (item) => ({
  type: 'ADD_ITEM',
  payload: item
});

                    

En este ejemplo, la acción addAction tiene un type llamado 'ADD_ITEM' y un payload que contiene el ítem que se va a agregar al estado.

Los reducers son funciones que determinan cómo cambia el estado en respuesta a una acción. Un reducer recibe el estado actual y una acción, y devuelve un nuevo estado. Es fundamental que los reducers sean funciones puras, lo que significa que no deben modificar el estado actual directamente, sino que deben devolver una copia nueva del estado con los cambios aplicados.


const initialState = {
  items: []
};

const itemReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload]
      };
    default:
      return state;
  }
};

                    

En este ejemplo, el reducer itemReducer gestiona un estado que contiene una lista de ítems. Cuando recibe una acción de tipo 'ADD_ITEM', devuelve un nuevo estado con el ítem añadido a la lista.

Middleware en Redux

A medida que las aplicaciones crecen, es común que necesites realizar operaciones más complejas como manejar acciones asíncronas. Aquí es donde entra en juego el concepto de middleware en Redux. El middleware es una capa intermedia que se coloca entre el dispatch de una acción y el momento en que esa acción llega al reducer. Esto te permite interceptar y modificar las acciones o realizar operaciones adicionales.

Uno de los middlewares más populares es redux-thunk, que permite despachar funciones asíncronas como acciones. Aquí tienes un ejemplo de cómo configurarlo:


import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;

                    

Con redux-thunk, puedes despachar funciones en lugar de objetos, lo que te permite manejar acciones que dependen de operaciones asíncronas como peticiones a una API:


const fetchItems = () => {
  return async (dispatch) => {
    const response = await fetch('/api/items');
    const data = await response.json();
    dispatch({
      type: 'SET_ITEMS',
      payload: data
    });
  };
};

                    

En este ejemplo, fetchItems es una función asíncrona que realiza una petición a una API y luego despacha una acción para actualizar el estado con los datos obtenidos.

Utilizar middleware como redux-thunk o redux-saga te permite manejar la lógica más compleja y mantener tu aplicación bien organizada y fácil de mantener.

Patrones y mejores prácticas

A medida que trabajas más con Redux, es importante adoptar ciertos patrones y mejores prácticas que te ayudarán a mantener tu código organizado, escalable y fácil de mantener. En esta sección, exploraremos algunos de estos patrones y cómo implementarlos en tus proyectos.

Estructuración de un proyecto con Redux

Organizar el código en un proyecto que utiliza Redux puede ser un desafío, especialmente en aplicaciones grandes. Una buena estructura de proyecto facilita la navegación y la comprensión del código. Aquí tienes una estructura recomendada:


/src
  /actions
    itemActions.js
  /reducers
    itemReducer.js
    rootReducer.js
  /components
    ItemList.js
    ItemDetail.js
  /containers
    ItemContainer.js
  /store
    store.js
  App.js
  index.js

                    

En esta estructura, las acciones y los reducers están organizados en carpetas separadas, lo que facilita su localización y mantenimiento. Los componentes que están conectados a Redux (a veces llamados containers) se separan de los componentes presentacionales, que son más generales y no están directamente conectados al store.

Selectors y memoización

A medida que el estado de la aplicación crece en tamaño y complejidad, extraer datos del store puede volverse ineficiente si no se manejan correctamente. Aquí es donde los selectors y la memoización entran en juego.

Un selector es simplemente una función que selecciona una parte específica del estado. Usar reselect, una biblioteca para crear selectors memoizados, puede mejorar significativamente el rendimiento de tu aplicación al evitar cálculos innecesarios:


import { createSelector } from 'reselect';

const selectItems = (state) => state.items;

const selectVisibleItems = createSelector(
  [selectItems, (state, filter) => filter],
  (items, filter) => items.filter(item => item.visible === filter)
);

                    

En este ejemplo, selectVisibleItems es un selector memoizado que filtra los ítems visibles en función de un filtro. La memoización asegura que si el estado o el filtro no cambian, el selector devolverá el resultado previamente calculado, mejorando así el rendimiento.

Normalización del estado

Otra práctica importante cuando trabajas con Redux es la normalización del estado. Normalizar el estado significa estructurarlo de una manera que elimine la duplicación de datos y facilite su acceso y manipulación. Esto es especialmente útil cuando manejas colecciones de objetos relacionados.

Considera un estado sin normalizar:


const state = {
  posts: [
    { id: 1, title: 'Post 1', author: { id: 1, name: 'Author 1' } },
    { id: 2, title: 'Post 2', author: { id: 2, name: 'Author 2' } }
  ]
};

                    

Este estado contiene duplicación de datos, lo que puede llevar a inconsistencias. Una versión normalizada podría verse así:


const state = {
  posts: {
    byId: {
      1: { id: 1, title: 'Post 1', author: 1 },
      2: { id: 2, title: 'Post 2', author: 2 }
    },
    allIds: [1, 2]
  },
  authors: {
    byId: {
      1: { id: 1, name: 'Author 1' },
      2: { id: 2, name: 'Author 2' }
    },
    allIds: [1, 2]
  }
};

                    

Este formato hace que sea más fácil actualizar, buscar y manipular datos, especialmente cuando el estado crece en complejidad. La normalización es una técnica poderosa que, combinada con selectors y middleware, puede llevar tu manejo del estado en Redux al siguiente nivel.

Integración con otras herramientas y tecnologías

Redux es extremadamente flexible y puede integrarse con una amplia gama de herramientas y tecnologías para mejorar la eficiencia y la experiencia de desarrollo. En esta sección, exploraremos cómo combinar Redux con React Hooks, así como algunas consideraciones al usar Redux con TypeScript.

Redux con React Hooks

Con la introducción de los Hooks en React, la forma en que se trabaja con Redux ha evolucionado. Hooks como useSelector y useDispatch permiten una integración más sencilla y natural de Redux en componentes funcionales.

useSelector es un hook que permite extraer datos del store, reemplazando la necesidad de usar connect. Aquí tienes un ejemplo básico:


import React from 'react';
import { useSelector } from 'react-redux';

const ItemList = () => {
  const items = useSelector(state => state.items);

  return (
    ul
      {items.map(item => (
        li key={item.id}>{item.name} /li
      ))}
    /ul
  );
};

export default ItemList;

                    

En este ejemplo, useSelector se utiliza para acceder a la lista de ítems directamente desde el store, lo que simplifica el código y reduce la necesidad de crear un componente contenedor separado.

useDispatch es otro hook que se usa para despachar acciones desde componentes funcionales. A continuación, un ejemplo sencillo:


import React from 'react';
import { useDispatch } from 'react-redux';

const AddItemButton = () => {
  const dispatch = useDispatch();

  const addItem = () => {
    dispatch({ type: 'ADD_ITEM', payload: { id: 3, name: 'New Item' } });
  };

  return button onClick={addItem} Add Item /button;
};

export default AddItemButton;

                    

Aquí, useDispatch se emplea para despachar una acción que agrega un nuevo ítem al store. Con los hooks, es posible escribir componentes más simples y con un código más limpio y conciso.

Redux y TypeScript

TypeScript es una excelente opción para proyectos grandes que usan Redux, ya que agrega tipado estático y ayuda a prevenir errores comunes. Sin embargo, la integración de Redux con TypeScript requiere algunas consideraciones especiales, especialmente en lo que respecta a las acciones y los reducers.

Para tipar correctamente las acciones en TypeScript, es común utilizar un patrón conocido como Action Creators. Aquí tienes un ejemplo:


interface AddItemAction {
  type: 'ADD_ITEM';
  payload: Item;
}

interface RemoveItemAction {
  type: 'REMOVE_ITEM';
  payload: number; // ID del ítem
}

type ItemActionTypes = AddItemAction | RemoveItemAction;

const addItem = (item: Item): AddItemAction => ({
  type: 'ADD_ITEM',
  payload: item
});

const removeItem = (id: number): RemoveItemAction => ({
  type: 'REMOVE_ITEM',
  payload: id
});

                    

En este código, se definen tipos específicos para las acciones y luego se crean Action Creators tipados, lo que asegura que las acciones despachadas cumplan con las expectativas del reducer.

Al definir reducers, es importante también tipar el estado y las acciones que maneja. Aquí tienes un ejemplo básico de un reducer en TypeScript:


interface ItemState {
  items: Item[];
}

const initialState: ItemState = {
  items: []
};

const itemReducer = (
  state = initialState,
  action: ItemActionTypes
): ItemState => {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload]
      };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload)
      };
    default:
      return state;
  }
};

                    

En este ejemplo, tanto el estado como las acciones están fuertemente tipados, lo que proporciona mayor seguridad y autocompletado en el entorno de desarrollo.

Usar TypeScript con Redux puede requerir un poco más de configuración, pero los beneficios en términos de detección temprana de errores y mejor documentación del código son invaluables, especialmente en proyectos a gran escala.

Casos de Uso y Ejemplos Prácticos

Entender la teoría de Redux es fundamental, pero donde realmente se ve su poder es en la práctica. En esta sección, exploraremos algunos casos de uso comunes y un ejemplo práctico que te guiará a través de la implementación de Redux en una aplicación simple.

Ejemplo práctico paso a paso

Vamos a construir una pequeña aplicación de lista de tareas usando Redux. Este ejemplo te mostrará cómo configurar Redux desde cero, crear acciones y reducers, y conectar todo con React.

1. Configuración del proyecto

Comienza creando un nuevo proyecto de React, si aún no lo tienes. Puedes hacerlo con Create React App:


npx create-react-app redux-todo-app
cd redux-todo-app

Luego, instala Redux y React-Redux:


npm install redux react-redux

2. Crear el Store y Reducer

Crea un archivo store.js en la carpeta src y configura el store básico de Redux:


import { createStore } from 'redux';

const initialState = {
  todos: []
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {
        ...state,
        todos: [...state.todos, action.payload]
      };
    case 'REMOVE_TODO':
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.payload)
      };
    default:
      return state;
  }
};

const store = createStore(reducer);

export default store;

Este reducer maneja dos acciones: ADD_TODO para agregar una nueva tarea y REMOVE_TODO para eliminar una tarea por su ID.

3. Conectar Redux a React

En tu archivo index.js, conecta Redux a React utilizando el componente Provider de react-redux:


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')
);

Esto asegura que el store de Redux esté disponible para todos los componentes de tu aplicación.

4. Crear Componentes y Conectar al Store

Ahora, crea un componente para mostrar la lista de tareas y otro para agregar nuevas tareas. Comencemos con el componente para mostrar las tareas:


import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

const TodoList = () => {
  const todos = useSelector(state => state.todos);
  const dispatch = useDispatch();

  const removeTodo = (id) => {
    dispatch({ type: 'REMOVE_TODO', payload: id });
  };

  return (
    ul
      {todos.map(todo => (
        li key={todo.id}
          {todo.text}
          button" onClick={() => removeTodo(todo.id)} Remove /button
        /li
      ))}
    /ul
  );
};

export default TodoList;

Este componente utiliza useSelector para obtener la lista de tareas del store y useDispatch para despachar una acción que elimina una tarea.

Ahora, crea un componente para agregar nuevas tareas:


import React, { useState } from 'react';
import { useDispatch } from 'react-redux';

const AddTodo = () => {
  const [text, setText] = useState('');
  const dispatch = useDispatch();

  const addTodo = () => {
    const newTodo = { id: Date.now(), text };
    dispatch({ type: 'ADD_TODO', payload: newTodo });
    setText('');
  };

  return (
    div
      input 
        type="text" 
        value={text} 
        onChange={(e) => setText(e.target.value)} 
      /
      button onClick={addTodo} Add Todo /button
    /div
  );
};

export default AddTodo;

Este componente maneja la creación de nuevas tareas y despacha la acción ADD_TODO para agregarlas al store.

5. Integrar todo en la aplicación

Finalmente, integra los componentes en tu aplicación principal App.js:


import React from 'react';
import TodoList from './TodoList';
import AddTodo from './AddTodo';

const App = () => (
  div
    h1 Todo List h1
    AddTodo /
    TodoList /
  /div
);

export default App;

Ahora, tu aplicación de lista de tareas está completa. Puedes agregar y eliminar tareas, y todo el estado se gestiona a través de Redux.

Patrones un poco más avanzados

Además de los ejemplos básicos, Redux es lo suficientemente flexible para manejar casos de uso más complejos como la gestión de formularios, la autenticación, o la paginación de datos. Estos patrones avanzados suelen implicar el uso de middleware como redux-thunk o redux-saga, y a menudo requieren un diseño cuidadoso del estado de la aplicación.

Por ejemplo, para manejar la autenticación, podrías tener un estado que almacena el usuario autenticado, y usar redux-thunk para gestionar el flujo de inicio de sesión, enviando las credenciales a un servidor y actualizando el estado basado en la respuesta.

De manera similar, si estás manejando grandes conjuntos de datos, puedes usar Redux para almacenar solo las páginas actuales de los datos y mantener un cache de las páginas ya cargadas, lo que permite una navegación eficiente.

Estos patrones avanzados son una muestra de cómo Redux puede ser adaptado para manejar casi cualquier necesidad en la gestión del estado de una aplicación compleja.

Conclusiones y recomendaciones

Redux es una poderosa herramienta para la gestión del estado en aplicaciones de JavaScript, especialmente en proyectos de gran envergadura donde la complejidad del estado puede volverse un desafío. A lo largo de este artículo, hemos explorado desde los conceptos básicos hasta patrones avanzados, así como algunas alternativas a Redux que podrían ser más adecuadas para ciertos proyectos.

Uno de los mayores beneficios de Redux es su predictibilidad. Al seguir un flujo de datos unidireccional y al mantener el estado inmutable, Redux facilita la depuración y la evolución de las aplicaciones. Esto es especialmente útil en equipos grandes donde varios desarrolladores están trabajando en el mismo código base.

Sin embargo, Redux también tiene una curva de aprendizaje que puede ser empinada para los desarrolladores que recién comienzan. Es importante evaluar si realmente necesitas la complejidad adicional que Redux introduce, especialmente en proyectos pequeños o medianos donde una solución más simple, como la Context API de React, podría ser suficiente.

Si decides usar Redux, aquí hay algunas recomendaciones clave:

  • Comienza simple: No intentes implementar todos los patrones avanzados desde el inicio. Comienza con una configuración básica y luego expande según las necesidades de tu proyecto.
  • Utiliza middleware cuando sea necesario: Middleware como redux-thunk o redux-saga puede simplificar la gestión de acciones asíncronas, pero no lo implementes a menos que realmente lo necesites.
  • Normaliza tu estado: A medida que el estado crece, la normalización ayuda a evitar la duplicación de datos y facilita la gestión del estado en aplicaciones grandes.
  • Aprovecha los hooks de React: Con useSelector y useDispatch, puedes simplificar tu código y hacerlo más legible.
  • Documenta bien tu código: Especialmente en proyectos grandes, una buena documentación y una clara estructura de carpetas ayudarán a tu equipo a entender y mantener el proyecto a largo plazo.

Redux no es la única herramienta disponible, pero es una de las más probadas y confiables. Si decides que es la mejor opción para tu proyecto, asegúrate de seguir las mejores prácticas y de estar dispuesto a iterar y mejorar tu implementación a medida que crece tu aplicación.

Finalmente, no dudes en explorar las alternativas que hemos mencionado, como MobX, Recoil o incluso la Context API, para encontrar la solución que mejor se adapte a tus necesidades. Cada proyecto es diferente, y lo más importante es elegir la herramienta que te permita desarrollar de manera eficiente y mantener un código de alta calidad.

Espero que este artículo te haya proporcionado una comprensión sólida de Redux y sus capacidades, así como de cuándo y cómo utilizarlo de manera efectiva. ¡Buena suerte en tus proyectos de desarrollo y feliz codificación!

Recursos adicionales

Estos recursos te proporcionarán una base sólida para continuar aprendiendo y experimentando con Redux, ayudándote a enfrentar los desafíos de la gestión del estado en tus proyectos de desarrollo.

Antonio Richaud

Soy un Data Scientist con experiencia en machine learning, deep learning y análisis financiero. Transformo grandes volúmenes de datos en insights y desarrollo soluciones que integran análisis avanzado con programación.