ReactRedux Toolkit

Redux Toolkit

Classic Redux required writing action type constants, action creators, and reducers separately — often spread across three files for a single feature. Redux Toolkit (RTK) is the official opinionated Redux starter that eliminates this boilerplate. It wraps Immer for immutable updates, combines actions and reducers into a single createSlice call, and ships with sane defaults for the store.

Installation

Bash
npm install @reduxjs/toolkit react-redux
configureStore

configureStore wraps Redux's createStore with good defaults: Redux DevTools is enabled automatically, and you combine multiple slice reducers in one object:

TS
// src/store/index.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../features/counter/counterSlice'
import usersReducer   from '../features/users/usersSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    users:   usersReducer,
  },
})

// Infer the RootState and AppDispatch types from the store itself
export type RootState  = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
createSlice — Actions + Reducer in One Place

createSlice generates action creators and the reducer from a single object. Under the hood it uses Immer, so you can write "mutating" syntax inside reducers and Immer converts it to a proper immutable update:

TS
// src/features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
  step: number
}

const initialState: CounterState = {
  value: 0,
  step: 1,
}

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    // Immer lets us "mutate" state directly — it produces an immutable update
    increment(state) {
      state.value += state.step
    },
    decrement(state) {
      state.value -= state.step
    },
    // PayloadAction types the action.payload
    incrementBy(state, action: PayloadAction<number>) {
      state.value += action.payload
    },
    setStep(state, action: PayloadAction<number>) {
      state.step = action.payload
    },
    reset(state) {
      state.value = 0
    },
  },
})

// createSlice auto-generates action creators with matching names
export const { increment, decrement, incrementBy, setStep, reset } =
  counterSlice.actions

export default counterSlice.reducer
useSelector and useDispatch

useSelector reads from the store; useDispatch returns a function to dispatch actions. For TypeScript projects, create typed hooks once and use them everywhere:

TSX
// src/store/hooks.ts — typed hooks (create once, use everywhere)
import { useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './index'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector = <T>(selector: (s: RootState) => T) =>
  useSelector(selector)

// ───────────────────────────────────────────────────────────
// src/features/counter/Counter.tsx
import { useAppSelector, useAppDispatch } from '../../store/hooks'
import { increment, decrement, setStep, reset } from './counterSlice'

export function Counter() {
  const { value, step } = useAppSelector(state => state.counter)
  const dispatch = useAppDispatch()

  return (
    <div>
      <h2>Counter: {value}</h2>
      <button onClick={() => dispatch(decrement())}>-</button>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(reset())}>Reset</button>
      <label>
        Step:
        <input
          type="number"
          value={step}
          onChange={e => dispatch(setStep(Number(e.target.value)))}
        />
      </label>
    </div>
  )
}
Providing the Store

TSX
// src/main.tsx (or index.tsx)
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import { store } from './store'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
)
Async Data: createAsyncThunk

RTK includes createAsyncThunk for async operations. It auto-generates pending, fulfilled, and rejected action types and handles the async flow for you:

TS
// src/features/users/usersSlice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

interface User { id: number; name: string; email: string }
interface UsersState {
  items: User[]
  status: 'idle' | 'loading' | 'succeeded' | 'failed'
  error: string | null
}

// createAsyncThunk generates: users/fetchAll/pending, /fulfilled, /rejected
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users')
  if (!res.ok) throw new Error('Failed to fetch users')
  return res.json() as Promise<User[]>
})

const usersSlice = createSlice({
  name: 'users',
  initialState: { items: [], status: 'idle', error: null } as UsersState,
  reducers: {},
  // extraReducers handles actions from outside this slice (e.g. thunks)
  extraReducers: builder => {
    builder
      .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.error.message ?? 'Unknown error'
      })
  },
})

export default usersSlice.reducer

TSX
// src/features/users/UserList.tsx
import { useEffect } from 'react'
import { useAppSelector, useAppDispatch } from '../../store/hooks'
import { fetchUsers } from './usersSlice'

export function UserList() {
  const dispatch = useAppDispatch()
  const { items, status, error } = useAppSelector(state => state.users)

  useEffect(() => {
    if (status === 'idle') dispatch(fetchUsers())
  }, [status, dispatch])

  if (status === 'loading')   return <p>Loading users…</p>
  if (status === 'failed')    return <p>Error: {error}</p>

  return (
    <ul>
      {items.map(user => (
        <li key={user.id}>{user.name} — {user.email}</li>
      ))}
    </ul>
  )
}
Redux DevTools

Install the Redux DevTools browser extension. configureStore enables it automatically. You get: a timeline of every dispatched action, a diff of state before/after each action, time-travel (jump to any past state), and action replay. This is one of Redux's strongest arguments over other solutions.

Recommended File Structure

Text
src/
  store/
    index.ts          ← configureStore + RootState/AppDispatch types
    hooks.ts          ← useAppSelector, useAppDispatch

  features/
    counter/
      counterSlice.ts ← createSlice (state + reducers + actions)
      Counter.tsx     ← React component that uses the slice
    users/
      usersSlice.ts   ← createSlice + createAsyncThunk
      UserList.tsx
      UserDetail.tsx
Note
The "feature folder" structure co-locates the slice and its components. This scales well — each feature is self-contained. You can move, delete, or hand off a feature without touching the rest of the codebase.
RTK Query Preview

RTK includes RTK Query — a powerful data-fetching and caching layer built into Redux Toolkit. It eliminates the need for createAsyncThunk for most data-fetching use cases. A dedicated page covers it in depth, but here's a taste:

TS
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const usersApi = createApi({
  reducerPath: 'usersApi',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  endpoints: builder => ({
    getUsers: builder.query({ query: () => '/users' }),
  }),
})

export const { useGetUsersQuery } = usersApi

// In a component:
function UserList() {
  const { data, isLoading } = useGetUsersQuery()
  // ...
}
When to Choose Redux
  • Large teams with many developers — strict action contracts prevent accidental state mutations.

  • Time-travel debugging is genuinely useful for your domain (finance, complex workflows).

  • Complex middleware — Redux Saga, custom logging, analytics event pipelines.

  • Existing Redux codebase — RTK is the upgrade path from classic Redux, not a rewrite.

  • For smaller apps, Zustand achieves 80% of the benefit with 20% of the setup.

Tip
If you're starting a new project, try Zustand first. If you find yourself needing middleware, strict action contracts, or Redux DevTools' time-travel, migrate to Redux Toolkit. RTK's createSlice API is clean enough that the migration is painless.