Redux / Redux Toolkit
Redux Toolkit (RTK)
Redux Toolkit is the official, recommended way to write Redux logic. It simplifies store setup, reduces boilerplate, and includes useful utilities.
Store setup
// 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
// 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
// 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
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
// 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
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
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
// 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)
)