Redux Toolkit
El conjunto de herramientas oficial y opinado para desarrollo eficiente de Redux
Redux Toolkit - La Forma Moderna de Redux
Redux Toolkit (RTK) es el conjunto de herramientas oficial y opinado para desarrollo eficiente de Redux. Simplifica la configuración del store, reduce el código repetitivo y incluye las mejores prácticas por defecto. Si estás usando Redux, deberías estar usando Redux Toolkit.
¿Qué Incluye RTK?
- configureStore() — Configuración simplificada del store con buenos valores predeterminados
- createSlice() — Genera reducers y action creators automáticamente
- createAsyncThunk() — Maneja lógica asíncrona con facilidad
- createEntityAdapter() — Gestiona datos normalizados
- RTK Query — Potente herramienta de obtención y caché de datos
Instalación
npm install @reduxjs/toolkit react-redux
createSlice - La Magia de RTK
import { createSlice } from '@reduxjs/toolkit';
// Un slice combina reducers y actions en un solo lugar
const todosSlice = createSlice({
name: 'todos',
initialState: {
items: [],
filter: 'all',
},
reducers: {
// Puedes escribir código "mutador" con Immer!
addTodo: (state, action) => {
state.items.push({
id: Date.now(),
text: action.payload,
completed: false,
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(t => t.id !== action.payload);
},
// Usar prepare callback para lógica personalizada del action creator
addTodoWithPrepare: {
reducer: (state, action) => {
state.items.push(action.payload);
},
prepare: (text) => {
return {
payload: {
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString(),
},
};
},
},
setFilter: (state, action) => {
state.filter = action.payload;
},
},
});
// Exportar actions generadas automáticamente
export const { addTodo, toggleTodo, deleteTodo, setFilter } = todosSlice.actions;
// Exportar el reducer
export default todosSlice.reducer;
configureStore
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './features/todos/todosSlice';
import userReducer from './features/user/userSlice';
const store = configureStore({
reducer: {
todos: todosReducer,
user: userReducer,
},
// Middleware y DevTools configurados automáticamente!
});
// Tipos de TypeScript para useSelector y useDispatch
export type RootState = ReturnType;
export type AppDispatch = typeof store.dispatch;
export default store;
createAsyncThunk - Lógica Asíncrona
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Define el thunk asíncrono
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
export const fetchUserById = createAsyncThunk(
'users/fetchUserById',
async (userId, { rejectWithValue }) => {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Error al obtener usuario');
return response.json();
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// Crea el slice con extraReducers para manejar los thunks
const usersSlice = createSlice({
name: 'users',
initialState: {
items: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null,
},
reducers: {
// Reducers regulares aquí
},
extraReducers: (builder) => {
builder
// Maneja el ciclo de vida de fetchUsers
.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;
})
// Maneja fetchUserById
.addCase(fetchUserById.fulfilled, (state, action) => {
const existingUser = state.items.find(u => u.id === action.payload.id);
if (existingUser) {
Object.assign(existingUser, action.payload);
} else {
state.items.push(action.payload);
}
});
},
});
export default usersSlice.reducer;
Uso en Componentes React
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo } from './todosSlice';
import { fetchUsers } from './usersSlice';
function TodoApp() {
const dispatch = useDispatch();
const todos = useSelector(state => state.todos.items);
const [text, setText] = useState('');
const handleAdd = () => {
if (text.trim()) {
dispatch(addTodo(text));
setText('');
}
};
return (
<div>
<input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
/>
<button onClick={handleAdd}>Agregar</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
onClick={() => dispatch(toggleTodo(todo.id))}
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
>
{todo.text}
</span>
<button onClick={() => dispatch(deleteTodo(todo.id))}>×</button>
</li>
))}
</ul>
</div>
);
}
function UsersList() {
const dispatch = useDispatch();
const { items: users, status, error } = useSelector(state => state.users);
useEffect(() => {
if (status === 'idle') {
dispatch(fetchUsers());
}
}, [status, dispatch]);
if (status === 'loading') return <div>Cargando...</div>;
if (status === 'failed') return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Hooks Tipados para TypeScript
// hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// Versiones tipadas de useDispatch y useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// Uso en componentes
function MyComponent() {
const dispatch = useAppDispatch();
const todos = useAppSelector(state => state.todos.items);
// ¡Seguridad de tipos completa!
}
Redux vs Redux Toolkit
| Característica | Redux Clásico | Redux Toolkit |
|---|---|---|
| Tipos de acción | Constantes manuales | Auto-generados |
| Action creators | Funciones manuales | Auto-generados |
| Inmutabilidad | Operadores spread | Immer (escribe mutaciones) |
| Configuración del store | Middleware manual | Buenos valores predeterminados incluidos |
| Lógica asíncrona | redux-thunk/saga | createAsyncThunk |
💡 Mejores Prácticas
- • Usa siempre Redux Toolkit para proyectos nuevos
- • Organiza el código por características (carpetas por feature)
- • Usa createAsyncThunk para llamadas a API
- • Aprovecha la sintaxis "mutable" de Immer
- • Considera RTK Query para obtención de datos
- • Usa hooks tipados en proyectos TypeScript