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:
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:
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:
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
:
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) {
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
export const selectProductBySearchText = (
state: RootState
) => ...
By convention, all the selection functions are prefixed with select
.
export default function Home() {
const searchProducts = useAppSelector(selectProductBySearchText);
...
You can also dispatch actions to update the store using the useAppDispatch
hook:
export default function Home() {
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.