Redux Toolkit

The official, modern way to write Redux logic with less boilerplate

Redux Toolkit (RTK)

Redux Toolkit is the official, opinionated, batteries-included toolset for efficient Redux development. It simplifies common Redux patterns and eliminates most of the boilerplate code. If you're using Redux today, you should be using Redux Toolkit.

What RTK Provides

  • configureStore — Simplified store setup with good defaults
  • createSlice — Generates action creators and reducers
  • createAsyncThunk — Handles async logic and loading states
  • createEntityAdapter — Normalized state management
  • RTK Query — Built-in data fetching and caching

Installation

# New project
npx create-react-app my-app --template redux

# Existing project
npm install @reduxjs/toolkit react-redux

createSlice - The Core of RTK

import { createSlice } from '@reduxjs/toolkit';

const todosSlice = createSlice({
  name: 'todos',
  initialState: {
    items: [],
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
  },
  reducers: {
    // RTK uses Immer - you can "mutate" state directly!
    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);
    },
    // Prepare callback for complex payloads
    addTodoWithId: {
      reducer: (state, action) => {
        state.items.push(action.payload);
      },
      prepare: (text) => ({
        payload: {
          id: crypto.randomUUID(),
          text,
          completed: false,
          createdAt: new Date().toISOString(),
        },
      }),
    },
  },
});

// Export auto-generated action creators
export const { addTodo, toggleTodo, deleteTodo, addTodoWithId } = todosSlice.actions;

// Export reducer
export default todosSlice.reducer;

configureStore

import { configureStore } from '@reduxjs/toolkit';
import todosReducer from './features/todos/todosSlice';
import userReducer from './features/user/userSlice';

export const store = configureStore({
  reducer: {
    todos: todosReducer,
    user: userReducer,
  },
  // DevTools enabled by default in development
  // Middleware includes redux-thunk by default
});

// TypeScript: Infer types from the store
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

Async Logic with createAsyncThunk

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// Create async thunk for fetching users
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async (_, { rejectWithValue }) => {
    try {
      const response = await fetch('/api/users');
      if (!response.ok) {
        throw new Error('Failed to fetch');
      }
      return await response.json();
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// Thunk with arguments
export const fetchUserById = createAsyncThunk(
  'users/fetchById',
  async (userId, { rejectWithValue }) => {
    try {
      const response = await fetch(`/api/users/${userId}`);
      return await response.json();
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    items: [],
    status: 'idle',
    error: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      // Handle fetchUsers lifecycle
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading';
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.items = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.payload;
      });
  },
});

export default usersSlice.reducer;

Using in React Components

import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo } from './todosSlice';
import { fetchUsers } from './usersSlice';

function TodoList() {
  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}>Add</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>Loading...</div>;
  if (status === 'failed') return <div>Error: {error}</div>;
  
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Typed Hooks for TypeScript

// hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from './store';

// Typed versions of useDispatch and useSelector
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

// Usage in components
function MyComponent() {
  const dispatch = useAppDispatch();
  const todos = useAppSelector(state => state.todos.items);
  // Full type safety!
}

Redux vs Redux Toolkit

Feature Classic Redux Redux Toolkit
Action types Manual constants Auto-generated
Action creators Manual functions Auto-generated
Immutability Spread operators Immer (write mutations)
Store setup Manual middleware Good defaults built-in
Async logic redux-thunk/saga createAsyncThunk

💡 Best Practices

  • • Always use Redux Toolkit for new projects
  • • Organize code by feature (feature folders)
  • • Use createAsyncThunk for API calls
  • • Take advantage of Immer's "mutable" syntax
  • • Consider RTK Query for data fetching
  • • Use typed hooks in TypeScript projects