¿Qué es Redux Toolkit?
Redux es un contenedor de estado predecible: un único store almacena el estado de tu aplicación, y la única forma de cambiarlo es despachando una acción que un reducer puro procesa. Esa restricción es lo que hace que Redux sea potente: cada cambio de estado es explícito, rastreable y testeable.
Durante años, Redux también fue famoso por su boilerplate. Redux Toolkit solucionó eso. Es la forma oficial y recomendada de escribir Redux hoy en día, agrupando configureStore, createSlice, Immer para actualizaciones inmutables, thunk middleware y devtools en un solo paquete. Obtienes la arquitectura sin toda la ceremonia.
El store y los slices
Un slice es la lógica del reducer y las acciones de una funcionalidad concentradas en un solo lugar. configureStore combina los slices en el store.
// app/store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
import todosReducer from "../features/todos/todosSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer,
},
});
// main.jsx
import { Provider } from "react-redux";
import { store } from "./app/store";
<Provider store={store}>
<App />
</Provider>;
createSlice genera los action creators y el reducer por ti, para que nunca tengas que escribir a mano las constantes de los tipos de acción. Immer potencia los reducers, lo que significa que puedes escribir código que parece una mutación mientras el estado permanece inmutable.
// features/todos/todosSlice.js
import { createSlice } from "@reduxjs/toolkit";
const todosSlice = createSlice({
name: "todos",
initialState: [],
reducers: {
add(state, action) {
state.push(action.payload);
},
toggle(state, action) {
const todo = state.find((t) => t.id === action.payload);
if (todo) todo.done = !todo.done;
},
remove(state, action) {
return state.filter((t) => t.id !== action.payload);
},
},
});
export const { add, toggle, remove } = todosSlice.actions;
export default todosSlice.reducer;
Lectura y despacho
Los componentes leen el estado con useSelector y envían acciones con useDispatch.
// TodoList.jsx
import { useSelector, useDispatch } from "react-redux";
import { add, toggle } from "../features/todos/todosSlice";
export function TodoList() {
const todos = useSelector((state) => state.todos);
const dispatch = useDispatch();
return (
<ul>
{todos.map((todo) => (
<li key={todo.id} onClick={() => dispatch(toggle(todo.id))}>
{todo.done ? "✓" : "○"} {todo.text}
</li>
))}
<button onClick={() => dispatch(add({ id: 1, text: "New" }))}>
Add
</button>
</ul>
);
}
Selecciona la porción más pequeña que necesites. useSelector vuelve a renderizar el componente cuando el valor seleccionado cambia, por lo que seleccionar todo el estado anularía la optimización. Utiliza createSelector de Reselect cuando un valor derivado se recalcularía en cada renderizado.
Lógica asíncrona con thunks
createAsyncThunk genera los tres tipos de acciones que produce una solicitud asíncrona, los cuales manejas en extraReducers.
// features/users/usersSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
export const fetchUsers = createAsyncThunk("users/fetch", async () => {
const res = await fetch("/api/users");
if (!res.ok) throw new Error("Failed to fetch users");
return res.json();
});
const usersSlice = createSlice({
name: "users",
initialState: { items: [], status: "idle", error: null },
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.status = "loading";
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = "succeeded";
state.items = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
},
});
Este es el patrón explícito de Redux: el estado de carga (loading), el éxito (success) y el fallo (failure) son acciones que puedes inspeccionar en las devtools.
RTK Query para el estado del servidor
Para la obtención de datos, RTK Query elimina por completo los thunks. Defines un API slice con endpoints, y este se encarga del caching, la deduplicación, la invalidación y el refetching.
// features/api/apiSlice.js
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
tagTypes: ["Post"],
endpoints: (builder) => ({
getPosts: builder.query({ query: () => "posts", providesTags: ["Post"] }),
addPost: builder.mutation({
query: (body) => ({ url: "posts", method: "POST", body }),
invalidatesTags: ["Post"],
}),
}),
});
export const { useGetPostsQuery, useAddPostMutation } = api;
// Posts.jsx
const { data, isLoading, error } = useGetPostsQuery();
RTK Query es la respuesta de Redux a TanStack Query: mantener el estado del servidor en un cache con un ciclo de vida, y mantener el estado del cliente en slices. Mezclar ambas responsabilidades en un mismo lugar es lo que hace que las aplicaciones de Redux se vuelvan caóticas.
Middleware y devtools
configureStore habilita la extensión Redux DevTools e incluye el middleware thunk por defecto. Puedes añadir tu propio middleware para logging, analíticas o efectos secundarios.
// middleware.js
const logger = (store) => (next) => (action) => {
console.log("dispatching", action.type);
return next(action);
};
export const store = configureStore({
reducer: rootReducer,
middleware: (getDefault) => getDefault().concat(logger),
});
Las devtools te permiten inspeccionar cada acción despachada, comparar el estado antes y después, y reproducir el historial — la razón principal por la cual los equipos eligen Redux para estados complejos.
Mejores prácticas
- Usa Redux Toolkit; nunca escribas Redux a mano.
- Mantén un slice por funcionalidad y agrupa la lógica relacionada.
- Selecciona la parte más pequeña posible del estado en
useSelector. - Usa
createSelectorpara datos derivados que requieran un cómputo costoso. - Mantén el estado del servidor en RTK Query, no en slices.
- Almacena únicamente datos serializables; mantén los valores no serializables fuera del store.
- Modela las acciones como eventos que ya ocurrieron, no como setters.
Errores comunes
- Seleccionar todo el estado y provocar un re-renderizado en cada cambio.
- Colocar datos del servidor en slices y gestionar los flags de carga manualmente.
- Almacenar valores no serializables, como instancias de clases o funciones.
- Mutar el estado fuera de un reducer.
- Usar Redux para el estado local de un componente que
useStatepodría manejar. - Escribir Redux legacy verboso en lugar de utilizar slices.
Próximos pasos
Redux Toolkit es la opción más madura para manejar estados de cliente grandes y complejos. Compáralo con la Context API integrada de React y con el más ligero Zustand, y delega las responsabilidades del servidor a TanStack Query o RTK Query. Para Vue, el rol equivalente lo cumple Pinia.