Redux / Redux Toolkit

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