APRENDE REDUX TOOLKIT: LA FORMA RECOMENDADA DE USAR REDUX EN APLICACIONES MODERNAS
Introducción a Redux Toolkit en el ecosistema actual
Redux Toolkit representa la evolución natural de Redux, diseñado para simplificar el desarrollo de aplicaciones que requieren una gestión de estado predecible y escalable. En el contexto de las aplicaciones web modernas, donde React sigue dominando el panorama frontend, esta herramienta se posiciona como la opción recomendada por el equipo oficial de Redux. Su enfoque opinado reduce la cantidad de código boilerplate necesario, permitiendo a los desarrolladores concentrarse en la lógica de negocio en lugar de en configuraciones repetitivas.
La biblioteca integra las mejores prácticas acumuladas a lo largo de los años, incluyendo utilidades como Immer para manejar mutaciones inmutables de forma segura. Además, incorpora funcionalidades comúnmente requeridas, como el manejo de acciones asíncronas y la configuración óptima del store. Esto hace que sea ideal para proyectos de cualquier tamaño, desde pequeñas aplicaciones hasta sistemas empresariales complejos que demandan un estado global robusto.
Instalación y configuración inicial del proyecto
Para comenzar a utilizar Redux Toolkit en una aplicación React, el primer paso consiste en instalar los paquetes necesarios. Actualmente, la versión estable más reciente es la 2.x, que incluye mejoras significativas en rendimiento y soporte para TypeScript.
Ejecuta el siguiente comando en tu terminal para agregar las dependencias:
npm install @reduxjs/toolkit react-redux
O, si prefieres yarn:
yarn add @reduxjs/toolkit react-redux
Una vez instalados, puedes crear un proyecto nuevo utilizando herramientas como Create React App o Vite, que ofrecen plantillas optimizadas para Redux.
Por ejemplo, con Vite:
npm create vite@latest my-redux-app -- --template react
cd my-redux-app
npm install
Luego instala las dependencias de Redux como se indicó anteriormente.
Creación de un slice para gestionar el estado
Un slice es una porción del estado global junto con sus reductores y acciones asociadas. Redux Toolkit proporciona la función createSlice, que elimina la necesidad de escribir manualmente action types y action creators.
Considera un ejemplo básico: un contador simple. Crea un archivo llamado counterSlice.js en una carpeta src/features/counter.
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
Gracias a Immer integrado, puedes escribir lógica que parece mutar el estado directamente, pero en realidad produce un nuevo estado inmutable. Esto simplifica enormemente el código, evitando spreads manuales profundos.
Configuración del store central
El store es el objeto central que mantiene el estado de la aplicación y permite el dispatch de acciones. Redux Toolkit ofrece configureStore, que configura automáticamente middleware útiles como redux-thunk y herramientas de desarrollo.
Crea un archivo src/app/store.js:
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
Esta configuración incluye por defecto el middleware para serialización y checks de inmutabilidad durante el desarrollo, ayudando a detectar errores comunes tempranamente.
Para aplicaciones más grandes, puedes combinar múltiples reductores utilizando combineReducers internamente, pero configureStore lo maneja de forma transparente.
Integración del store en la aplicación React
Para hacer que el store esté disponible en toda la jerarquía de componentes React, utiliza el componente Provider de react-redux.
En tu archivo principal, generalmente src/index.js o src/main.jsx:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { store } from "./app/store";
import { Provider } from "react-redux";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
Con esto, cualquier componente descendiente puede acceder al estado global y dispatch acciones sin prop drilling.
Uso de hooks para interactuar con el store
React-Redux proporciona hooks modernos que reemplazan el antiguo connect. Los más utilizados son useSelector y useDispatch.
En un componente funcional, por ejemplo src/features/counter/Counter.js:
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { decrement, increment, incrementByAmount } from "./counterSlice";
export function Counter() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState("2");
return (
<div>
<div>
<button
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<span>{count}</span>
<button
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
<div>
<input
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<button
onClick={() =>
dispatch(
incrementByAmount(Number(incrementAmount) || 0)
)
}
>
Add Amount
</button>
</div>
</div>
);
}
useSelector permite seleccionar porciones específicas del estado, y React-Redux optimiza las re-renderizaciones solo cuando cambian los valores seleccionados.
Manejo de lógica asíncrona con createAsyncThunk
Muchas aplicaciones requieren operaciones asíncronas, como llamadas a APIs. Redux Toolkit introduce createAsyncThunk para manejar estas situaciones de forma elegante, generando automáticamente acciones para estados pending, fulfilled y rejected.
Crea un thunk para fetch un contador desde una API ficticia. En counterSlice.js, agrega:
import { createAsyncThunk } from '@reduxjs/toolkit';
// Thunk asíncrono
export const incrementAsync = createAsyncThunk(
'counter/fetchCount',
async (amount) => {
const response = await new Promise((resolve) =>
setTimeout(() => resolve({ data: amount }), 1000)
);
return response.data;
}
);
// En extraReducers del slice
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = 'loading';
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = 'idle';
state.value += action.payload;
})
.addCase(incrementAsync.rejected, (state) => {
state.status = 'failed';
});
},
Actualiza initialState para incluir status:
const initialState = {
value: 0,
status: "idle",
};
En el componente, dispatch el thunk:
<button onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}>
Add Async
</button>
Y muestra el estado de carga:
const status = useSelector((state) => state.counter.status);
{
status === "loading" && <p>Loading...</p>;
}
Este patrón maneja automáticamente el ciclo de vida de las promesas, facilitando la gestión de loading, errores y datos.
Organización de proyectos grandes con múltiples slices
En aplicaciones reales, el estado se divide en múltiples dominios. Crea carpetas por feature, cada una con su slice, componentes y selectores.
Por ejemplo, agrega un slice para posts:
// src/features/posts/postsSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchPosts = createAsyncThunk("posts/fetchPosts", async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
return response.json();
});
const postsSlice = createSlice({
name: "posts",
initialState: {
items: [],
status: "idle",
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchPosts.pending, (state) => {
state.status = "loading";
})
.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = "succeeded";
state.items = action.payload;
});
},
});
export default postsSlice.reducer;
Luego, agrega al store:
import postsReducer from '../features/posts/postsSlice';
reducer: {
counter: counterReducer,
posts: postsReducer,
},
Esta estructura escalable mantiene el código organizado y modular.
Selectores y memoización avanzada
Para optimizar lecturas del estado, utiliza createSelector de reselect, incluido en Redux Toolkit.
Ejemplo en postsSlice:
import { createSelector } from "@reduxjs/toolkit";
const selectPosts = (state) => state.posts.items;
export const selectPostById = createSelector(
[selectPosts, (state, postId) => postId],
(posts, postId) => posts.find((post) => post.id === postId)
);
Esto evita cálculos innecesarios cuando el estado no cambia relevantemente.
Mejores prácticas en el uso diario
Al trabajar con Redux Toolkit, sigue estas recomendaciones para mantener un código limpio y mantenible. Limita las mutaciones lógicas a los slices, evitando lógica de estado en componentes. Utiliza los devtools de Redux para inspeccionar acciones y estado durante el desarrollo.
Además, considera tipar tu store con TypeScript para mayor seguridad. Redux Toolkit ofrece excelente soporte nativo, con inferencia automática de tipos en slices y thunks.
En entornos de producción, configureStore desactiva automáticamente checks costosos para mejor rendimiento.
Integración con otras bibliotecas modernas
Redux Toolkit se integra perfectamente con frameworks como Next.js, donde puedes configurar el store en páginas o app router. También funciona bien con bibliotecas de UI como Material UI o Tailwind, manteniendo el estado separado de la presentación.
Para manejo avanzado de datos, aunque no cubierto aquí en profundidad, RTK Query ofrece una solución poderosa para caching y fetching, construida sobre los mismos principios.
Depuración y herramientas de desarrollo
Las Redux DevTools vienen preconfiguradas con configureStore. Instala la extensión del navegador para visualizar el timeline de acciones, diff de estado y replay. Esto acelera significativamente la identificación de problemas en la lógica de reducers.
Ejemplos extendidos para escenarios comunes
Considera un caso de autenticación. Crea un authSlice con login async:
export const login = createAsyncThunk("auth/login", async (credentials) => {
// Llamada a API real
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify(credentials),
});
return response.json();
});
// En extraReducers manejar token, user, etc.
Luego, protege rutas basadas en useSelector((state) => state.auth.user).
Otro ejemplo común es un carrito de compras, donde slices manejan items, totals calculados con selectores memoizados.
Consideraciones de rendimiento
Redux Toolkit está optimizado para rendimiento. Los hooks de React-Redux utilizan shallow equality checks, y los selectores memoizados evitan re-renders innecesarios. En aplicaciones grandes, divide el estado en slices pequeños para granularidad fina.
Evita almacenar datos no serializables directamente, ya que middleware por defecto advierte sobre esto.
Migración desde Redux clásico
Si vienes de Redux vanilla, la transición es sencilla. Reemplaza switch statements con createSlice, thunks manuales con createAsyncThunk, y connect con hooks. La mayoría del código antiguo sigue funcionando, pero adoptar las APIs modernas mejora notablemente la productividad.
Conclusiones
Redux Toolkit consolida las mejores prácticas en una sola biblioteca, haciendo que la gestión de estado en React sea más accesible y eficiente. Al reducir boilerplate y proporcionar herramientas poderosas como createSlice y createAsyncThunk, permite construir aplicaciones robustas con menos esfuerzo. Su adopción generalizada en la comunidad refleja su madurez y efectividad en proyectos reales. Incorporarlo en tu flujo de trabajo elevará la calidad y mantenibilidad de tus desarrollos frontend, preparándote para escalar aplicaciones complejas con confianza.