Olasunkanmi John Ajiboye TypeScript and Rust enthusiast. Writes code for humans. From the land of Promise.

Using TypeScript with Redux Toolkit

4 min read 1386

Using-TypeScript-with-Redux-Toolkit

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:

Dan-Abramov-Redux-Tweet

We don’t just write about Redux, we talk about it too. Listen now:

Or subscribe for later

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 and useSelector 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.

Project Structure Using Template Redux Typescript
Our project structure.

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.

We made a custom demo for .
No really. Click here to check it out.

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&lt) => {
            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 =&gt; async (dispatch) =&gt; {
    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 =&gt; async (dispatch) =&gt; {
    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) =&gt; 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.

: Full visibility into your web 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 apps.

.
Olasunkanmi John Ajiboye TypeScript and Rust enthusiast. Writes code for humans. From the land of Promise.

3 Replies to “Using TypeScript with Redux Toolkit”

Leave a Reply