Redux / Redux Toolkit
21 Jan, 2026
On this page
Redux Toolkit (RTK) Jump to heading
Redux Toolkit is the official, recommended way to write Redux logic. It simplifies store setup, reduces boilerplate, and includes useful utilities.
Store setup Jump to heading
// store.ts
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './features/counter/counterSlice'
export const store = configureStore({
reducer: {
counter: counterReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
Creating a slice Jump to heading
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
interface CounterState {
value: number
}
const initialState: CounterState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
Typed hooks Jump to heading
// hooks.ts
import { useDispatch, useSelector, TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch } from './store'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
Using in components Jump to heading
import { useAppSelector, useAppDispatch } from '../../hooks'
import { increment, decrement, incrementByAmount } from './counterSlice'
export const Counter = () => {
const count = useAppSelector((state) => state.counter.value)
const dispatch = useAppDispatch()
return (
<div>
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
</div>
)
}
Provider setup Jump to heading
// main.tsx or App.tsx
import { Provider } from 'react-redux'
import { store } from './store'
const App = () => (
<Provider store={store}>
<YourApp />
</Provider>
)
Async logic with createAsyncThunk Jump to heading
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
interface User {
id: string
name: string
}
interface UsersState {
entities: User[]
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
error: string | null
}
const initialState: UsersState = {
entities: [],
loading: 'idle',
error: null,
}
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await fetch('/api/users')
return response.json() as Promise<User[]>
})
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = 'pending'
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = 'succeeded'
state.entities = action.payload
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = 'failed'
state.error = action.error.message ?? 'Something went wrong'
})
},
})
export default usersSlice.reducer
RTK Query Jump to heading
For data fetching, RTK Query is built into Redux Toolkit and handles caching, loading states, and more.
// services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
interface Post {
id: number
title: string
body: string
}
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (builder) => ({
getPosts: builder.query<Post[], void>({
query: () => 'posts',
providesTags: ['Post'],
}),
getPost: builder.query<Post, number>({
query: (id) => `posts/${id}`,
}),
addPost: builder.mutation<Post, Partial<Post>>({
query: (body) => ({
url: 'posts',
method: 'POST',
body,
}),
invalidatesTags: ['Post'],
}),
}),
})
export const { useGetPostsQuery, useGetPostQuery, useAddPostMutation } = api
Add the API reducer and middleware to your store:
import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware),
})
Use in components:
import { useGetPostsQuery, useAddPostMutation } from './services/api'
const Posts = () => {
const { data: posts, isLoading, error } = useGetPostsQuery()
const [addPost, { isLoading: isAdding }] = useAddPostMutation()
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error loading posts</div>
return (
<div>
<button onClick={() => addPost({ title: 'New Post' })} disabled={isAdding}>
Add Post
</button>
{posts?.map((post) => (
<div key={post.id}>{post.title}</div>
))}
</div>
)
}
Selectors Jump to heading
// Inline selector
const count = useAppSelector((state) => state.counter.value)
// Reusable selector
export const selectCount = (state: RootState) => state.counter.value
// With createSelector for memoization
import { createSelector } from '@reduxjs/toolkit'
const selectUsers = (state: RootState) => state.users.entities
const selectActiveFilter = (state: RootState) => state.filters.active
export const selectActiveUsers = createSelector(
[selectUsers, selectActiveFilter],
(users, isActive) => users.filter((user) => user.active === isActive)
)
← Back home