TechLead

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