Why Redux Toolkit with TypeScript
Redux Toolkit is the official, recommended way to write Redux logic. It simplifies common Redux concerns including boilerplate, configuration, and middleware. Combined with TypeScript, it provides excellent type safety for global state management for your web applications.
Redux Toolkit was created to address three common concerns about Redux:
- "Configuring a Redux store is too complicated"
- "I have to add a lot of packages to get Redux to do anything useful"
- "Redux requires too much boilerplate code"
According to the official Redux Toolkit documentation, these pain points led to the creation of RTK as a batteries-included solution that provides sensible defaults out of the box.
Redux Toolkit vs Redux
The key difference is that Redux Toolkit provides sensible defaults and built-in utilities that eliminate the boilerplate while maintaining full type safety. RTK uses Immer internally, allowing you to write "mutating" logic that gets converted to immutable updates.
Why type safety matters for production applications
Compile-Time Error Detection
Catch type mismatches before runtime, preventing production bugs in state management logic.
Autocomplete Support
Get intelligent suggestions for state properties, action types, and dispatch methods.
Refactoring Confidence
Safely rename properties and restructure state knowing the compiler will catch breaking changes.
Self-Documenting Code
Types serve as living documentation for state shape and action structures.
Installation and Setup
Package Installation
Install both Redux Toolkit and React bindings:
npm install @reduxjs/toolkit react-redux
# or
yarn add @reduxjs/toolkit react-redux
As documented in the Redux Toolkit Getting Started guide, these two packages provide everything you need to start using Redux with React.
TypeScript Version Requirements
Redux Toolkit follows DefinitelyTyped's policy of supporting TypeScript versions released within the past two years. As of RTK 2.x, this means:
- RTK 2.x requires TypeScript 5.4+
- RTK 1.9.x requires TypeScript 4.7+
If you're unable to upgrade TypeScript, RTK may still work with older versions, but you may encounter type errors or missing type inference. The TypeScript usage documentation provides detailed information on version compatibility.
For optimal type safety and the best developer experience, always use the minimum required TypeScript version or higher. When building modern React applications, having the latest TypeScript version ensures you benefit from improved type inference and language features.
1import { configureStore } from '@reduxjs/toolkit'2import rootReducer from './reducers'3 4const store = configureStore({5 reducer: rootReducer6})7 8export type RootState = ReturnType<typeof store.getState>9export type AppDispatch = typeof store.dispatch10 11// Typed hooks for React components12export const useAppDispatch = () => useDispatch<AppDispatch>()13export const useAppSelector: TypedUseSelectorHook<RootState> = useSelectorTyped Dispatch and Hooks
Creating typed useDispatch and useSelector hooks is essential for type-safe React-Redux integration:
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
These typed hooks ensure that:
- Dispatch knows exactly what actions can be dispatched
- Selector knows the complete state shape and provides autocomplete
- Components receive proper type checking for Redux interactions
As outlined in the Redux Toolkit TypeScript documentation, this pattern prevents runtime errors by catching type mismatches at compile time.
1import { createSlice, PayloadAction } from '@reduxjs/toolkit'2 3interface CounterState {4 value: number5}6 7const initialState: CounterState = {8 value: 09}10 11const counterSlice = createSlice({12 name: 'counter',13 initialState,14 reducers: {15 increment(state) {16 state.value += 117 },18 decrement(state) {19 state.value -= 120 },21 incrementByAmount(state, action: PayloadAction<number>) {22 state.value += action.payload23 }24 }25})26 27export const { increment, decrement, incrementByAmount } = counterSlice.actions28export default counterSlice.reducerCreating Type-Safe Slices
Using createSlice
The createSlice API automatically generates action creators and action types with proper TypeScript support. The key is using PayloadAction<T> for actions that carry data:
incrementByAmount(state, action: PayloadAction<number>) {
state.value += action.payload
}
This typing ensures:
- action.payload is typed as
number - TypeScript will error if you try to assign a non-number
- IDE autocomplete suggests the correct payload type
According to the Redux Toolkit TypeScript patterns documentation, this approach provides complete type safety for slice reducers.
Defining State and Action Types
Best practice is to:
- Define a TypeScript interface for your slice state
- Use it as the type for
initialState - Export the state interface for use in selectors
- Use
PayloadAction<PayloadType>for reducer arguments
By following these patterns in your React web development projects, you ensure maintainable and type-safe state management across your application.
1import { createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'2 3interface User {4 id: number5 name: string6}7 8interface UserResponse {9 users: User[]10}11 12// Async thunk with typed generics:13// ReturnType, ArgumentType, ThunkApiConfig14export const fetchUsers = createAsyncThunk<15 UserResponse,16 void,17 { rejectValue: string }18>(19 'users/fetchUsers',20 async (_, { rejectWithValue }) => {21 try {22 const response = await fetch('/api/users')23 if (!response.ok) {24 return rejectWithValue('Failed to fetch users')25 }26 return await response.json()27 } catch (error) {28 return rejectWithValue(29 error instanceof Error ? error.message : 'Unknown error'30 )31 }32 }33)Async Operations with createAsyncThunk
Async Thunk Pattern
createAsyncThunk simplifies async logic with three generic parameters:
- ReturnType: What the async function returns on success
- ArgumentType: What the thunk receives when dispatched
- ThunkApiConfig: Configuration including
rejectValuetype
createAsyncThunk<
UserResponse, // Return type on success
void, // Argument type
{ rejectValue: string } // Config with error type
>
As described in the Redux Toolkit TypeScript documentation, these generics ensure complete type safety throughout the async operation lifecycle.
Handling Async States
Use extraReducers with the builder callback pattern to handle the lifecycle:
const userSlice = createSlice({
name: 'users',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true
state.error = null
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false
state.users = action.payload.users
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false
state.error = action.payload || 'Unknown error'
})
}
})
1import { createSelector } from '@reduxjs/toolkit'2import { RootState } from './store'3 4interface CartItem {5 id: string6 price: number7 quantity: number8}9 10interface CartState {11 items: CartItem[]12}13 14// Base selector15const selectCartItems = (state: RootState): CartItem[] => 16 state.cart.items17 18// Memoized derived selector19export const selectCartTotal = createSelector(20 [selectCartItems],21 (items) => items.reduce(22 (total, item) => total + item.price * item.quantity, 23 024 )25)26 27// Another memoized selector28export const selectCartItemCount = createSelector(29 [selectCartItems],30 (items) => items.reduce((count, item) => count + item.quantity, 0)31)Memoized Selectors with createSelector
Performance Optimization
createSelector from the Reselect library (re-exported by RTK) creates memoized selectors that only recompute when their input selectors return different values:
export const selectCartTotal = createSelector(
[selectCartItems],
(items) => items.reduce((total, item) => total + item.price * item.quantity, 0)
)
Without memoization:
- Every render recalculates derived values
- All components using the selector re-render
With memoization:
- Only recomputes when
cart.itemsactually changes - Prevents needless re-renders, improving performance
The Redux Toolkit selector patterns demonstrate how createSelector optimizes derived data calculations.
1import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'2 3interface Post {4 id: number5 title: string6 body: string7}8 9export const postsApi = createApi({10 reducerPath: 'postsApi',11 baseQuery: fetchBaseQuery({ baseUrl: '/api' }),12 endpoints: (builder) => ({13 getPosts: builder.query<Post[], void>({14 query: () => 'posts'15 }),16 getPostById: builder.query<Post, number>({17 query: (id) => `posts/${id}`18 }),19 createPost: builder.mutation<Post, Partial<Post>>({20 query: (newPost) => ({21 url: 'posts',22 method: 'POST',23 body: newPost24 })25 })26 })27})28 29// Auto-generated hooks with full TypeScript support30export const {31 useGetPostsQuery,32 useGetPostByIdQuery,33 useCreatePostMutation34} = postsApiRTK Query with TypeScript
Data Fetching Made Simple
RTK Query is a powerful data fetching and caching solution included in Redux Toolkit. Using TypeScript with createApi provides excellent type safety:
Endpoint Typing:
builder.query<ResultType, QueryArg>for GET requestsbuilder.mutation<ResultType, BodyType>for POST/PUT/DELETE
Type Inference:
- TypeScript automatically infers hook return types
- Query arguments are properly typed
- Error types are inferred from the API configuration
As detailed in the RTK Query TypeScript documentation, this approach eliminates the need for manual type definitions while maintaining complete type safety. For applications requiring efficient API integrations, RTK Query provides a robust solution that handles caching, polling, and optimistic updates out of the box.
1// Counter component with typed Redux hooks2import { useAppSelector, useAppDispatch } from './hooks'3import { increment } from './counterSlice'4 5function Counter() {6 const count = useAppSelector((state) => state.counter.value)7 const dispatch = useAppDispatch()8 9 return (10 <button onClick={() => dispatch(increment())}>11 Count: {count}12 </button>13 )14}15 16// Posts component with RTK Query hooks17import { useGetPostsQuery } from './services/postsApi'18 19function PostsList() {20 const { data, error, isLoading } = useGetPostsQuery()21 22 if (isLoading) return <div>Loading...</div>23 if (error) return <div>Error loading posts</div>24 25 return (26 <ul>27 {data?.map((post) => (28 <li key={post.id}>{post.title}</li>29 ))}30 </ul>31 )32}Best Practices and Common Patterns
Organizing Slice Files
Recommended structure for scalable Redux applications:
src/
features/
counter/
counterSlice.ts # Slice with actions & reducer
counterSelectors.ts # Custom selectors
counterTypes.ts # Type definitions
store/
store.ts # Store configuration
hooks.ts # Typed React-Redux hooks
Type Safety Throughout the App
- Export types from slices for reuse
- Use RootState for all selector functions
- Create typed hooks in a dedicated hooks file
- Avoid any types - be explicit about state shape
Avoiding Common TypeScript Pitfalls
- Circular references: Define types in separate files when needed
- Missing exports: Export action types for use in components
- Generic inference: Be explicit with createAsyncThunk generics
- Strict null checks: Account for optional state properties
Testing Type-Safe Redux Code
Type safety doesn't replace testing, but it catches many errors early:
- Test reducer logic with known state and actions
- Test async thunks with mocked API responses
- Test selectors with various input states
- Test component integration with typed hooks
Frequently Asked Questions
Do I need TypeScript to use Redux Toolkit?
No, Redux Toolkit works without TypeScript. However, TypeScript provides significant benefits including compile-time error detection, autocomplete support, and refactoring confidence. Since RTK is written in TypeScript, you get excellent type inference even in JavaScript projects.
When should I use createSelector?
Use createSelector when you need to derive data from the store and want to avoid recalculating on every render. It's especially valuable for expensive computations like filtering, sorting, or aggregating data from large state objects.
RTK Query vs createAsyncThunk - which should I use?
Use RTK Query for server state (API data, caching, synchronization). Use createAsyncThunk for client-side async operations that don't fit the REST pattern. Many applications use both - RTK Query for data fetching and createAsyncThunk for operations like form submissions.
How do I type complex state with multiple slices?
Combine slice types using TypeScript's intersection types. Define each slice's state type, then create a RootState type that combines them. Export each type from its slice file for reusability in selectors and components.
Can I use Redux Toolkit with React Server Components?
Yes, but with considerations. Client components can use hooks normally. For server components, you'll need to access the store differently or use context-based patterns. Next.js has specific recommendations for Redux integration.
Conclusion
Redux Toolkit with TypeScript provides a powerful, type-safe foundation for state management in React applications. Key takeaways:
| Pattern | Purpose |
|---|---|
configureStore | Typed store setup with middleware |
| Typed hooks | Type-safe useDispatch and useSelector |
PayloadAction<T> | Typed action payloads in reducers |
createAsyncThunk | Async operations with type safety |
createSelector | Memoized derived state |
createApi | Type-safe data fetching with RTK Query |
Start with typed hooks and createSlice for basic state management. Add RTK Query for data fetching needs. Use createAsyncThunk for complex async workflows that don't fit RTK Query's patterns.
For comprehensive documentation, refer to the official Redux Toolkit Getting Started guide, TypeScript usage patterns, and RTK Query TypeScript integration.
Implementing these patterns in your React applications ensures maintainable, type-safe state management that scales with your project.