Redux with Typescript for State Management

26 July 2022[updated]

5 min read

Web development

Redux with Typescript is an amazing combination. You may think of Redux as a complex state management solution that involves a lot of boilerplate code. Rest assured! Redux is improved, and Redux with Typescript will solve all your state management problems!

In this article we are going to learn how to use Redux with Typescript. We will install Redux and Redux toolkit and use it to setup a production-level state management solution for your web application.


Installation

We are going to use redux-toolkit, which is the recommended way of implementing redux in 2021.

We start by installing redux-toolkit and react-redux:

npm install @reduxjs/toolkit react-redux

Folder structure

The new recommended way of structuring your redux folders is to create a features folder and divide the files based on each feature of the application.

We create a features folder in the root of the project.

In this example, we are going to define a slice for a search box. This slice will implement the logic for searching content in our website. We create a search folder inside features, and a search-slice.ts file inside it.

The search-slice.ts file will have the following code:

search-slice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';

// Define the state of the slice as an object
interface SearchState {
  searchText: string;
}

// Define an initial state
const initialState: SearchState = {
  searchText: '',
};

// Create a slice containing the configuration of the state
// and the reducers functions
const searchSlice = createSlice({
  name: 'search',
  initialState,
  reducers: {
    updateSearchText(state, action: PayloadAction<string>) {
      state.searchText = action.payload.toLowerCase();
    },
  },
});

// Export each reducers function defined in createSlice
export const { updateSearchText } = searchSlice.actions;

// Export default the slice reducer
export default searchSlice.reducer;

That's all the code you need! createSlice takes a name, an initial state, and reducers. It will then take care of the rest. No more huge complex files full of boilerplate code!

As you can see, you can export the reducers functions from searchSlice. These functions have been created by createSlice based on the reducers you added to the slice.

createSlice uses immer under the hood. Therefore, it is possible to mutate state inside each reducers function. immer will convert all the mutation in a new immutable state.

Create the store

We create a new folder called app in the root of the project. This folder will contain the store file and the custom hooks used inside the components to interact with the state.

First we create a store.ts file inside app with the following content:

store.ts
import { configureStore } from '@reduxjs/toolkit';

// Import the previously created search slice
import searchSlice from '../features/search/search-slice';

// Create the store, adding the search slice to it
export const store = configureStore({
  reducer: {
    search: searchSlice,
  },
});

// Export some helper types used to improve type-checking
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;

Redux has one global store which contains all your slices (reducer). We create the store using the configureStore function. We also export the types generated from our store (AppDispatch and RootState).

Custom hooks

We also create a hooks.ts file inside the app folder. This file defines some custom hooks specific for the project that help to improve auto-completion and type-checking:

hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from './store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

We are going to use these two hooks throughout the whole project to interact with the store using Typescript.

Add Provider

Finally, we add the Provider component inside _app.tsx containing the store:

_app.tsx
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { store } from '../app/store';
import { ReactElement } from 'react';

import { Provider } from 'react-redux';

function MyApp({ Component, pageProps }: AppProps): ReactElement {
  return (
    <Provider store={store}>
      <Component {...pageProps} />;
    </Provider>
  );
}
export default MyApp;

The Provider component will manage and provide the store we created to our entire application.

Read the store using hooks

Now you can read the store from your components using the useAppSelector hook.

useAppSelector takes a function that has as input the store (of type RootState) and returns the specific state that you need from the store. It is recommended to define the selection functions inside each *-slice.ts file:

search-slice.ts
// search-slice.ts
export const selectProductBySearchText = (
  state: RootState
) => ...

By convention, all the selection functions are prefixed with select.

export default function Home(): ReactElement {
  const searchProducts = useAppSelector(selectProductBySearchText);
	...

You can also dispatch actions to update the store using the useAppDispatch hook:

export default function Home(): ReactElement {
  const searchProducts = useAppSelector(selectProductBySearchText);
  const dispatch = useAppDispatch();
	...

Then use the dispatch function to update the store:

import { updateSearchText } from '../features/search/search-slice';
...

onChange={(e) => dispatch(updateSearchText(e.target.value))}

Believe it or not, that's all you need! You now have Redux configured in your application. You can create new features and new slices and add it to the store.

If you learned something from the article, follow me on Twitter at @SandroMaglione and subscribe to my newsletter here below for more tutorials and guides 👇

Thanks for reading.