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
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:
// 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.dispatchcreateSlice — 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:
// 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.reduceruseSelector 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:
// 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
// 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:
// 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// 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
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.tsxRTK 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:
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.