State management in React with Redux can be incredibly challenging. Aside from wrapping your head around the core concepts and the architectural patterns, you also have an enormous amount of boilerplates that can make it even more daunting and confusing. Dan Abramov, the creator of Redux, describes the frustration better than I:
Redux Toolkit to the rescue
With just the right degree of abstractions, Redux Toolkit is one of the more successful attempts to make working with Redux less intimidating and more intuitive. It is easy to scale and adapt on large distributed projects. However, that’s not the main gist of this post.
You can go into deeper detail on why Redux Toolkit is a smart choice. We’ll instead concentrate on how to use RTK with TypeScript.
Combining the well-thought-out approach of Redux Toolkit and the type-safety of TypeScript will yield a more robust, maintainable, and scalable Redux project. However, it is not always straightforward to set up RTK with TypeScript — and that’s what we’ll attempt to illuminate in this article.
What we’ll cover in this post
- Installations and initial setup
- Configuring the store
- How to structure your Redux project
- Creating action reducers with
createSlice
- Async with thunk, error handling, and loading states
- Connecting to store using
useDispatch
anduseSelector
Hooks
Installations and initial setup
If you are just starting out on a React-Redux, project setting up is easy with create-react-app
. The --template redux-typescript
flag does the trick!
npx create-react-app my-app --template redux-typescript //or yarn create-react-app my-app --template redux-typescript
You should end up with a project structure that looks like the below. Of particular interest is the app
directory: it contains the store
and the feature
directory, which should hold the major feature of the app as sub-directories. We’ll come back to these later.

Setting up in an existing project
It is equally easy to drop Redux Toolkit into an existing React project. Of course, you’d need to install all the peer dependencies:
npm install @types/react-redux react-redux @reduxjs/toolkit
RTK exposes the configureStore
API, which is much easier to set up than a traditional Redux store. You can provide arrays of middleware and enhancers; applyMiddleware
and compose
are automatically called for you.
Configuring the store with configureStore
The simplest way is to set up a store with a root reducer. Create src/app/rootReducer.ts
and src/app/store.ts
and add the following:
// src/app/rootReducer.ts import { combineReducers } from '@reduxjs/toolkit' const rootReducer = combineReducers({}) export type RootState = ReturnType export default rootReducer
We have set up an empty rootReducer
, which is where we’ll add all our reducers, like below:
const rootReducer = combineReducers({ oneReducer, anotherReducer, yetAnotherReducer })
We will also set up RootState
in the store, which will be used for selectors
and action dispatch
later on. When we type individual states and actions, we get a strongly typed store correctly inferred. More on this later!
// src/app/store.ts import { configureStore, Action } from '@reduxjs/toolkit' import { useDispatch } from 'react-redux' import { ThunkAction } from 'redux-thunk' import rootReducer, { RootState } from './rootReducer' const store = configureStore({ reducer: rootReducer, }) export type AppDispatch = typeof store.dispatch export const useAppDispatch = () => useDispatch() export type AppThunk = ThunkAction<void, RootState, unknown, Action> export default store
It’s that simple — we have now successfully set up our Redux store. You can also configure and add middlewares to the store:
import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'; import logger from 'redux-logger'; const middleware = [...getDefaultMiddleware(), logger]; export default configureStore({ reducer, middleware, });
How to structure your Redux project
Since RTK is pretty opinionated, it recommends the “feature folder” structure. Of course, you are free to use whatever structure works best for you, but I have personally found this structure quite comprehensible. Let’s say you have a GitHub issues tracker, just like the example in the official docs. You’d have a folder structure that looks like this:
Creating action reducers with createSlice
Remember the combinedReducer
from the store? We will now create our first reducer with createSlice
. Imagine this is an authReducer
for an app; any reducer will follow the same flow, depending on your custom logic.
// src/features/auth/authSlice.ts import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AppThunk } from '../../app/store' import { RootState } from '../../app/rootReducer' export interface AuthError { message: string } export interface AuthState { isAuth: boolean currentUser?: CurrentUser isLoading: boolean error: AuthError } export interface CurrentUser { id: string display_name: string email: string photo_url: string } export const initialState: AuthState = { isAuth: false, isLoading: false, error: {message: 'An Error occurred'}, } export const authSlice = createSlice({ name: 'auth', initialState, reducers: { setLoading: (state, {payload}: PayloadAction<) => { state.isLoading = payload }, setAuthSuccess: (state, { payload }: PayloadAction) => { state.currentUser = payload state.isAuth = true }, setLogOut: (state) => { state.isAuth = false state.currentUser = undefined }, setAuthFailed: (state, { payload }: PayloadAction) => { state.error = payload state.isAuth = false }, }, }) export const { setAuthSuccess, setLogOut, setLoading, setAuthFailed} = authSlice.actions export const authSelector = (state: RootState) => state.auth
We have passed the CurrentUser
type as a generic to PayloadAction
to ensure a correctly typed store.
Notice one key issue here? All the methods in the slice are synchronous. In reality, we’d need to talk to an API asynchronously to confirm the auth state. We will cover this later in the async thunk section, but visualizing the state in this synchronous manner helps understand the data flow.
RTK also automatically generates actions; we can then destructure them out of the authSlice.actions
object later on.
The selectors, in this case, authSelector
will enable us to get a slice of the store in any part of the component tree. We’ll come back to this later.
Async actions with thunk, error handling, and loading states
Under the hood, RTK uses redux-thunk
for handling async logic. You can switch to redux-saga
if you want, or other alternatives. Let’s look at handling async logic in our authSlice
.
//src/features/auth/authSlice.ts ---- export const login = (): AppThunk => async (dispatch) => { try { dispatch(setLoading(true)) const currentUser = getCurrentUserFromAPI('https://auth-end-point.com/login') dispatch(setAuthSuccess(currentUser)) } catch (error) { dispatch(setAuthFailed(error)) } finally { dispatch(setLoading(false)) } } export const logOut = (): AppThunk => async (dispatch) => { try { dispatch(setLoading(true)) await endUserSession('https://auth-end-point.com/log-out') } catch (error) { dispatch(setAuthFailed(error)) } finally { dispatch(setLoading(false)) } } export const authSelector = (state: RootState) => state.auth export default authSlice.reducer
Though I found the above approach simple enough, another approach is to use createAsyncThunk
. This generates promise lifecycle action types based on the action type prefix that you pass in. It then returns a thunk action creator that will run the promise callback and dispatch the lifecycle actions based on the returned promise.
Connecting to the store using the useDispatch
and useSelector
Hooks
Let’s hook up our app to the store and see how we can dispatch actions and select any slice of the store.
import React from 'react' import { login, logOut, authSelector, CurrentUser } from './auth' import { useSelector, useDispatch } from 'react-redux' import './App.css' export function App() { const dispatch = useDispatch() const { currentUser, isLoading, error, isAuth } = useSelector(authSelector) if (isLoading) return <div>....loading</div> if (error) return <div>{error.message}</div> return ( <div className="App"> {isAuth ? ( <button onClick={() => dispatch(logOut)}> Logout</button> ) : ( <button onClick={() => dispatch(login)}>Login</button> )} <UserProfile user={currentUser}/> </div> ) } interface UserProfileProps { user?: CurrentUser } function UserProfile({ user }: UserProfileProps) { return <div>{user?.display_name}</div> }
Conclusion
We have seen how we can use Redux Toolkit with TypeScript for type-safe store, dispatch, and actions. With the createSlice
API, we are able to easily set up the store with just a few lines of code. The useSelector
Hook enables us to select any slice of the store at any point within the component tree, while useDispatch
allows the dispatching of an action that in turn updates the store.
LogRocket: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Try it for free.
Hey, nice article. When you have a time, take a look at this lib I built https://www.npmjs.com/package/react-simple-reducer
It makes dead simple to use Typescript in a Redux Toolkit way.
I believe the code in the “Async actions with thunk, error handling, and loading states” section is the same as the last one…
Thanks, just updated
Can we use redux toolkit with React typescript class component? Since useDispatch and useSelector are hooks