A Guide To Redux Toolkit With TypeScript

The Redux Toolkit documentation calls the library a better way to write Redux logic for React apps and a simple and efficient toolkit for Redux development. In this article, you will learn about the Redux toolkit by building an app that tracks project issues.

If you are a React developer working on a complex application, you will need to use global state management for your app at some point. React Redux is one of the most popular libraries for state management used by many developers. However, React Redux has a complex setup process that I’ve found inefficient, not to mention it requires a lot of boilerplate code. The official developer of Redux developed the Redux Toolkit to simplify the process.

This article is for those with enough knowledge of React and TypeScript to work with Redux.

About Redux

Redux is the global state management library for React applications. If you have used useState() hooks for managing your app state, you will find it hard to access the state when you need it in the other parts of the application. With useState() hooks, the state can be passed from the parent component to the child, and you will be stuck with the problem of prop drilling if you need to pass it to multiple children. That’s where Redux comes in to manage the application state.

Redux Toolkit is a set of opinionated and standardised tools that simplify application development using the Redux state management library.

The primary benefit of using Redux Toolkit is that it removes the overhead of writing a lot of boilerplates like you’d have to do with plain Redux.

It eliminates the need to write standard Redux setup code, such as defining actions, reducers, and store configuration, which can be a significant amount of code to write and maintain.

Jerry Navi has a great tutorial that shows the full Redux setup process.

The Redux Toolkit has several key features which make me use this library over plain Redux:

  1. Defining reducers
    With Redux Toolkit, you can specify a slice with a few lines of code to define a reducer instead of defining actions and reducers separately, like Redux.
  2. Immutability helpers
    Redux Toolkit includes a set of utility functions that make it easy to update objects and arrays in an immutable way. This makes writing code that follows the Redux principles of immutability simpler.
  3. Built-in middleware
    Redux Toolkit includes built-in middleware that can handle asynchronous request tasks.
  4. DevTools integration
    Redux Toolkit includes integration with the Redux DevTools browser extension, which makes it easier to debug and analyse Redux code.

I think the best way to explain the value and benefits of using Redux Toolkit is simply to show them to you in a real-world context. So, let’s develop an app with it that is designed to create and track GitHub issues.

Project issue tracker
(Large preview)

You can follow along with the code examples as we go and reference the full code anytime by grabbing it from GitHub. There is also a live deployment of this example that you can check out.

Start creating a new React app with the following command:

yarn create react-app project_issue_tracker --template typescript

This generates a folder for our project with the basic files we need for development. The –template typescript part of the command is used to add TypeScript to the stack.

Now, let’s install the dependencies packages required for our project and build the primary UI for the application before we implement Redux Toolkit. First, navigate to the project_issue_tracker project folder we just created:

cd project_issue_tracker

Then run the following command to install Material UI and Emotion, where the former is a design library we can use to style components, and the latter enables writing CSS in JavaScript files.

yarn add @mui/material @emotion/react @emotion/styled

Now we can install Redix Toolkit and Redux itself:

yarn add @reduxjs/toolkit react-redux

We have everything we need to start developing! We can start by building the user interface.

More after jump! Continue reading below ↓

Developing The User Interface

In this section, we will be developing the UI of the app. Open the main project folder and create a new components subfolder directly in the root. Inside this new folder, create a new file called ProjectCard.tsx. This is where we will write the code for a ProjectCard component that contains information about an open issue in the project issue tracker.

Let’s import some design elements from the Material UI package we installed to the new /components/ProjectCard.tsx file to get us started:

import React from "react";
import { Typography, Grid, Stack, Paper} from "@mui/material";
interface IProps { issueTitle: string
}
const ProjectCard : React.FC<IProps> = ({ issueTitle }) => { return( <div className="project_card"> <Paper elevation={1} sx={{p: '10px', m:'1rem'}}> <Grid container spacing={2}> <Grid item xs={12} md={6}> <Stack spacing={2}> <Typography variant="h6" sx={{fontWeight: 'bold'}}> Issue Title: {issueTitle} </Typography> <Stack direction='row' spacing={2}> <Typography variant="body1"> Opened: yesterday </Typography> <Typography variant="body1"> Priority: medium </Typography> </Stack> </Stack> </Grid> </Grid> </Paper> </div> )
}
export default ProjectCard;

This creates the project card that displays an issue title, issue priority level, and the time the issue was “opened.” Notice that we are using an issueTitle prop that will be passed to the ProjectCard component to render the issue with a provided title.

Now, let’s create the component for the app’s HomePage to display all the issues. We’ll add a small form to the page for submitting new issues that contain a text field for entering the issue name and a button to submit the form. We can do that by opening up the src/HomePage.tsx file in the project folder and importing React’s useState hook, a few more styled elements from Material UI, and the ProjectCard component we set up earlier:

import React, { useState } from "react";
import { Box, Typography, TextField, Stack, Button } from "@mui/material";
import ProjectCard from "./components/ProjectCard";
const HomePage = () => { const [textInput, setTextInput] = useState(''); const handleTextInputChange = (e:any) => { setTextInput(e.target.value); }; return( <div className="home_page"> <Box sx={{ml: '5rem', mr: '5rem'}}> <Typography variant="h4" sx={{textAlign: 'center'}}> Project Issue Tracker </Typography> <Box sx={{display: 'flex'}}> <Stack spacing={2}> <Typography variant="h5"> Add new issue </Typography> <TextField id="outlined-basic" label="Title" variant="outlined" onChange={handleTextInputChange} value={textInput} /> <Button variant="contained">Submit</Button> </Stack> </Box> <Box sx={{ml: '1rem', mt: '3rem'}}> <Typography variant="h5" > Opened issue </Typography> <ProjectCard issueTitle="Bug: Issue 1" /> <ProjectCard issueTitle="Bug: Issue 2" /> </Box> </Box> </div> )
}
export default HomePage;

This results in a new HomePage component that a user can interact with to add new issues by entering an issue name in a form text input. When the issue is submitted, a new ProjectCard component is added to the HomePage, which acts as an index for viewing all open issues.

The only thing left for the interface is to render the HomePage, which we can do by adding it to the App.tsx file. The full code is available here on GitHub.

Now that our UI is finalised, we can move on to implementing Redux Toolkit to manage the state of this app. We will use Redux Toolkit to manage the state of the ProjectCard list by storing all the issues in a store that can be accessed from anywhere in the application.

Before we move to the actual implementation, let’s understand a few Redux Toolkit concepts to help understand what we’re implementing:

  1. createSlice
    This function makes it easy to define the reducer, actions, and the initialState under one object. Unlike the plain redux, you don’t need to use a switch for actions and need to define the actions separately. This function accepts an object as a name (i.e., the name of the slice) and the initial state of the store and the reducer, where you define all the reducers along with their action types.
  2. configureStore
    This function is an abstraction for the Redux createStore() function. It removes the dependency of defining reducers separately and creating a store again. This way, the store is configured automatically and can be passed to the Provider.
  3. createAsyncThunk
    This function simplifies making asynchronous calls. It automatically dispatches many different actions for managing the state of the calls and provides a standardised way to handle errors.

Let’s implement all of this! We will create the issueReducer with an addIssue() action that adds any new submitted issue to the projectIssues store. This can be done by creating a new file in src/redux/ called IssueReducer.ts with this code:

// Part 1
import { createSlice, PayloadAction } from "@reduxjs/toolkit" // Part 2
export interface IssueInitialState { projectIssues: string[]
}
const initialState: IssueInitialState = { projectIssues: []
} // Part 3
export const issueSlice = createSlice({ name: 'issue', initialState, reducers: { addIssue: (state, action: PayloadAction<string>) => { state.projectIssues = [...state.projectIssues, action.payload] } }
}) // Part 4
export const { addIssue } = issueSlice.actions
export default issueSlice.reducer

Let’s understand each part of the code. First, we are importing the necessary functions from the Redux @reduxjs/toolkit package.

Then, we create the type definition of our initial state and initialise the initialState for the issueReducer. The initialState has a projectIssues[] list that will be used to store all the submitted issues. We can have as many properties defined in the initialState as we need for the application.

Thirdly, we are defining the issueSlice using Redux Toolkit’s createSlice function, which has the logic of the issueReducer as well as the different actions associated with it. createSlice accepts an object with a few properties, including:

  • name: the name of the slice,
  • initialState: the initial state of the reducer function,
  • reducers: an object that accepts different actions we want to define for our reducer.

The slice name for the issueReducer is issueSlice. The initalState of it is defined, and a single adIssue action is associated with it. The addIssue action is dispatched whenever a new issue is submitted. We can have other actions defined, too, if the app requires it, but this is all we need for this example.

Finally, in the last part of the code, we export the actions associated with our reducer and the issueSlice reducer. We have fully implemented our issueReducer, which stores all the submitted issues by dispatching the addIssue action.

Now let’s configure the issueReducer in our store so we can use it in the app. Create a new file in src/redux/ called index.ts, and add the following code:

import { configureStore } from "@reduxjs/toolkit";
import IssueReducer from "./IssueReducer";
export const store = configureStore({ reducer: { issue: IssueReducer }
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch

This code configures and creates the store using the configureStore() function that accepts a reducer where we can pass all of the different reducers.

We are done adding the reducer and configuring the store with Redux Toolkit. Let’s do the final step of passing the store to our app. Start by updating the App.tsx file to pass the store using the Provider:

import React from 'react';
import { Provider } from "react-redux"
import { store } from './redux';
import HomePage from './HomePage';
function App() { return ( <div className="App"> <Provider store={store}> <HomePage /> </Provider> </div> );
}
export default App;

Here, you can see that we are importing the store and directly passing through the Provider. We don’t need to write anything extra to create a store or configure DevTools like we would using plain Redux. This is definitely one of the ways Redux Toolkit streamlines things.

OK, we have successfully set up a store and a reducer for our app with Redux Toolkit. Let’s use our app now and see if it works. To quickly sum things up, the dispatch() function is used to dispatch any actions to the store, and useSelector() is used for accessing any state properties.

We will dispatch the addIssue action when the form button is clicked:

const handleClick = () => { dispatch(addIssue(textInput))
}

To access the projectIssue list stored in our reducer store, we can make use of useSelector() like this:

const issueList = useSelector((state: RootState) => state.issue.projectIssues)

Finally, we can render all the issues by map()-ping the issueList to the ProjectCard component:

{ issueList.map((issue) => { return( <ProjectCard issueTitle={issue} /> ) })
}

The final code for HomePage.tsx looks like this:

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "./redux/index"
import { Box, Typography, TextField, Stack, Button } from "@mui/material";
import ProjectCard from "./components/ProjectCard";
import { addIssue } from "./redux/IssueReducer";
const HomePage = () => { const dispatch = useDispatch(); const issueList = useSelector((state: RootState) => state.issue.projectIssues) const [textInput, setTextInput] = useState(''); const handleTextInputChange = (e:any) => { setTextInput(e.target.value); }; const handleClick = () => { dispatch(addIssue(textInput)) } return( <div className="home_page"> <Box sx={{ml: '5rem', mr: '5rem'}}> <Typography variant="h4" sx={{textAlign: 'center'}}> Project Issue Tracker </Typography> <Box sx={{display: 'flex'}}> <Stack spacing={2}> <Typography variant="h5"> Add new issue </Typography> <TextField id="outlined-basic" label="Title" variant="outlined" onChange={handleTextInputChange} value={textInput} /> <Button variant="contained" onClick={handleClick}>Submit</Button> </Stack> </Box> <Box sx={{ml: '1rem', mt: '3rem'}}> <Typography variant="h5" > Opened issue </Typography> { issueList.map((issue) => { return( <ProjectCard issueTitle={issue} /> ) }) } </Box> </Box> </div> )
}
export default HomePage;

Now, when we add and submit an issue using the form, that issue will be rendered on the homepage.

This section covered how to define any reducer and how they’re used in the app. The following section will cover how Redux Toolkit makes asynchronous calls a relatively simple task.

We implemented our store to save and render any newly added issue to our app. What if we want to call GitHub API for any repository and list all the issues of it in our app? In this section, we will see how to use the createAsyncThunk() API with the slice to get data and render all the repository issues using an API call.

I always prefer to use the createAsyncThunk() API of the redux toolkit because it standardises the way different states are handled, such as loading, error, and fulfilled. Another reason is that we don’t need to add extra configurations for the middleware.

Let’s add the code for creating a GithubIssue reducer first before we break it down to understand what’s happening. Add a new GithubIssueReducer.ts file in the /redux folder and add this code:

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const fetchIssues = createAsyncThunk<string[], void, { rejectValue: string }>( "githubIssue/fetchIssues", async (_, thunkAPI) => { try { const response = await fetch("https://api.github.com/repos/github/hub/issues"); const data = await response.json(); const issues = data.map((issue: { title: string }) => issue.title); return issues; } catch (error) { return thunkAPI.rejectWithValue("Failed to fetch issues."); } }
);
interface IssuesState { issues: string[]; loading: boolean; error: string | null;
}
const initialState: IssuesState = { issues: [], loading: false, error: null,
};
export const issuesSliceGithub = createSlice({ name: 'github_issues', initialState, reducers: {}, extraReducers: (builder) => { builder .addCase(fetchIssues.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchIssues.fulfilled, (state, action) => { state.loading = false; state.issues = action.payload; }) .addCase(fetchIssues.rejected, (state, action) => { state.loading = false; state.error = action.error.message || 'Something went wrong'; }); },
});
export default issuesSliceGithub.reducer;

Let’s understand the fetchIssues part first:

  1. We are using the createAsyncThunk() API provided by the Redux Toolkit. It helps create asynchronous actions and handles the app’s loading and error states.
  2. The action type name is the first argument passed to createAsyncThunk(). The specific action type name we have defined is githubIssue/fetchIssues.
  3. The second argument is a function that returns a Promise, which resolves to the value that dispatches the action. This is when the asynchronous function fetches data from a GitHub API endpoint and maps the response data to a list of issue titles.
  4. The third argument is an object that contains configuration options for the async thunk. In this case, we have specified that the async thunk will not be dispatched with any arguments (hence the void type) and that if the Promise returned by the async function is rejected, the async thunk will return an action with a rejected status along with a rejectValue property that contains the string “Failed to fetch issues.”

When this action is dispatched, the API calls will be made, and the githubIssuesList data will be stored. We can follow this exact same sequence of steps to make any API calls we need.

The second section of the code is similar to what we used when we created the issueSlice, but with three differences:

  1. extraReducers
    This object contains the reducers logic for the reducers not defined in the createSlice reducers object. It takes a builder object where different cases can be added using addCase for specific action types.
  2. addCase
    This method on the builder object creates a new case for the reducer function.
  3. API call states
    The callback function passed to the addCase method is dispatched by createAsyncThunk(), which updates the different store objects based on the API call states (pending, fulfilled, and error).

We can now use the GithubIssue reducer actions and the store in our app. Let’s add the GithubIssueReducer to our store first. Update the /redux/index.ts file with this code:


import { configureStore } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import IssueReducer from "./IssueReducer";
import GithubIssueReducer from "./GithubIssueReducer";
export const store = configureStore({ reducer: { issue: IssueReducer, githubIssue: GithubIssueReducer }
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()

We just added the GithubIssueReducer to our store with the name mapped to githubIssue. We can now use this reducer in our HomePage component to dispatch the fetchIssues() and populate our page with all the issues received from the GitHub API repo.

import React, { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { useAppDispatch, RootState, AppDispatch } from "./redux/index";
import { Box, Typography, TextField, Stack, Button } from "@mui/material";
import ProjectCard from "./components/ProjectCard";
import { addIssue } from "./redux/IssueReducer";
import { fetchIssues } from "./redux/GithubIssueReducer";
const HomePage = () => { const dispatch: AppDispatch = useAppDispatch(); const [textInput, setTextInput] = useState(''); const githubIssueList = useSelector((state: RootState) => state.githubIssue.issues) const loading = useSelector((state: RootState) => state.githubIssue.loading); const error = useSelector((state: RootState) => state.githubIssue.error); useEffect(() => { dispatch(fetchIssues()) }, [dispatch]); if (loading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error}</div>; } const handleTextInputChange = (e:any) => { setTextInput(e.target.value); }; const handleClick = () => { console.log(textInput) dispatch(addIssue(textInput)) } return( <div className="home_page"> <Box sx={{ml: '5rem', mr: '5rem'}}> <Typography variant="h4" sx={{textAlign: 'center'}}> Project Issue Tracker </Typography> <Box sx={{display: 'flex'}}> <Stack spacing={2}> <Typography variant="h5"> Add new issue </Typography> <TextField id="outlined-basic" label="Title" variant="outlined" onChange={handleTextInputChange} value={textInput} /> <Button variant="contained" onClick={handleClick}>Submit</Button> </Stack> </Box> <Box sx={{ml: '1rem', mt: '3rem'}}> <Typography variant="h5" > Opened issue </Typography> { githubIssueList?.map((issue : string) => { return( <ProjectCard issueTitle={issue} /> ) }) } </Box> </Box> </div> )
}
export default HomePage;

This updates the code in HomePage.tsx with two minor changes:

  1. We dispatch fetchIssue and use the createAsync() action to make the API calls under the useEffect hook.
  2. We use the loading and error states when the component renders.

Now, when loading the app, you will first see the “Loading” text rendered, and once the API call is fulfilled, the issuesList will be populated with all the titles of GitHub issues fetched from the repo.

Once again, the complete code for this project can be found on GitHub. You can also check out a live deployment of the app, which displays all the issues fetched from GitHub.

Conclusion

There we have it! We used Redux Toolkit in a React TypeScript application to build a fully functional project issue tracker that syncs with GitHub and allows us to create new issues directly from the app.

We learned many of the foundational concepts of Redux Toolkit, such as defining reducers, immutability helpers, built-in middleware, and DevTools integration. I hope you feel powered to use Redux Toolkit effectively in your projects. With Redux Toolkit, you can improve the performance and scalability of your React applications by effectively managing the global state.

Further Reading on Smashing Magazine

Smashing Editorial
(gg, yk, il)