What is Redux?
Redux is a state management library commonly used with JavaScript applications, particularly those built with React. It helps manage and centralize the state of the application in a predictable way. Here are some of the core concepts:
- Store – The single source of truth for the application’s state.
- Actions – Plain JavaScript objects that represent an intention to change the state.
- Reducers – Determine how the state should change in response to an action.
- Dispatch – Used to send actions to the store that will update the state accordingly.
- Immutability – The state in Redux is immutable, meaning it cannot be changed directly.
In the evolving landscape of web development, efficiently managing your application’s state is crucial. Redux has long been a popular choice for state management in React applications, but it often comes with a steep learning curve and lots of boilerplate code.
Let me introduce Redux Toolkit, a powerful set of tools and best practices designed to simplify Redux development.
What is Redux Toolkit?
Redux Toolkit (RTK) is the official, recommended way to write Redux logic. It addresses common pain points by providing a set of tools that simplify the setup and development process, including:
- configureStore: Simplifies store setup with good default settings.
- createSlice: Reduces boilerplate by combining reducers, action creators, and action types.
- createAsyncThunk: Simplifies handling asynchronous actions and side effects.
- createEntityAdapter: Standardizes the management of normalized data in the Redux state.
- RTK Query: A powerful tool for data fetching and caching.
Why TypeScript?
TypeScript brings static typing to JavaScript, providing numerous benefits such as:
- Enhanced code readability and maintainability.
- Better tooling and auto-completion in IDEs.
Combining Redux Toolkit with TypeScript allows you to leverage these benefits while managing your application state more effectively.
To setup everything firstly we need to install few packages:
npm install typescript @reduxjs/toolkit react-redux
Add tsconfig.json in project root. Here is the example configuration:
{
"compilerOptions": {
"baseUrl": "src",
"target": "ES6",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"noFallthroughCasesInSwitch": true,
"types": ["jest", "node"]
},
"include": [
"src"
]
}
Great! We have the initial setup ready. Now we need to configure our store inside of our application.
Recommended folder structure for redux store:
store/ │ ├── assets/ │ ├── builders.ts │ ├── slice.ts │ ├── thunks.ts │ └── types.ts │ ├── hooks.ts │ ├── store.ts
Creating Types
Create a types.ts file to add assets state type:
type Nullable <T> = T | null;
export type Asset {
id: string;
name: string;
}
type GetAssets {
data: Asset[];
isLoading: boolean;
error: Nullable<string>;
}
export type AssetsState {
getAssets: GetAssets;
selectedAsset: Nullable<Asset>;
}
Handling API Asynchronous Calls
Create a thunks.ts file to handle API asynchronous calls:
import { createAsyncThunk } from '@reduxjs/toolkit';
export const getAssets = createAsyncThunk(
'assets/getAssets', async (_, thunkAPI) => {
try {
const response = await AssetService.getAssets();
return response.data;
} catch (error) {
return thunkAPI.rejectWithValue({ error: error.message });
}
}
);
Handling Async Thunks
Create a builders.ts file for handling asyncThunks:
import { ActionReducerMapBuilder } from '@reduxjs/toolkit';
import { AssetsState } from './types';
import { getAssets } from 'store/assets/thunks';
export const getAssetsBuilder = (builder: ActionReducerMapBuilder<AssetsState>): void => {
builder.addCase(getAssets.pending, state => {
state.getAssets.isLoading = true;
state.getAssets.data = [];
state.getAssets.error = null;
});
builder.addCase(getAssets.fulfilled, (state, action) => {
state.getAssets.isLoading = false;
state.getAssets.data = action.payload;
state.getAssets.error = null;
}
);
builder.addCase(getAssets.rejected, (state, action) => {
state.getAssets.isLoading = false;
state.getAssets.data = [];
state.getAssets.error = action.payload;
});
};
Handling Assets slice logic
Create a slice.ts file for handling assets slice logic:
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AssetsState } from './types';
const initialState: AssetsState = {
selectedAsset: null,
getAssets: {
isLoading: false,
data: [],
error: null
}
}
const assetsSlice = createSlice({
name: 'assets',
initialState: initialState,
reducers: {
// for synchronous actions
setSelectedAsset: (state, action: PayloadAction<Nullable<Asset>>) => {
state.selectedAsset = action.payload;
}
},
builders: builder => {
// for handling of asynchronous actions
getAssetsBuilder(builder);
}
});
export { setSelectedAsset } = assetsSlice.actions;
export default assetsSlice;
Configuring Your Store
Create a store.ts file to configure your store:
import { configureStore } from '@reduxjs/toolkit';
import assetsSlice from 'store/assets/assetsSlice';
export const store = configureStore({
reducer: {
[assetsSlice.name]: assetsSlice.reducer
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
Creating Hooks
Create a hooks.ts file to create AppDispatch and AppSelector that will be used for selecting data from state. This is useful to have for IntelliSense autocomplete when using AppSelector.
import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { AppDispatch, RootState } from './store';
export const useAppDispatch = (): ThunkDispatch<RootState, null, AnyAction> =>
useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
We are finished with the setup!
Using Redux Toolkit in a Component
Let me show an example how to use it inside of a component:
import React, { FC } from 'react';
import { useAppSelector, useAppDispatch } from 'store/hooks';
import { getAssets } from 'store/assets/thunks';
import { setSelectedAsset } from 'store/assets/slice';
const Assets: FC = () => {
const dispatch = useAppDispatch();
// redux state data
const {
selectedAsset, getAssets: { isLoading, data, error }
} = useAppSelector(state => state.assets)
const setAsset = (asset: Asset): void => {
dispatch(setSelectedAsset(asset));
}
useEffect(() => {
dispatch(getAssets());
}, []);
return (
<div>
{selectedAsset && <div>Selected asset: {selectedAsset.name}</div>}
{data.map((asset) => (
<div onClick={() => setAsset(asset)}>{asset.name}</div>
))}
</div>
)
}
export default Assets;
Bonus feature: configureStore from Redux toolkit has Redux DevTools Extension turned on by default!
Redux Toolkit, when combined with TypeScript, offers a powerful, type-safe approach to state management in React applications. By reducing boilerplate and providing advanced tools for handling asynchronous actions, Redux Toolkit allows developers to focus on building feature-rich, scalable applications. Embrace these patterns and techniques to elevate your Redux development experience.
Good luck and Happy coding!
“Redux Toolkit with Typescript” Tech Bite was brought to you by Arif Mahmić, Senior Software Engineer at Atlantbh.
Tech Bites are tips, tricks, snippets or explanations about various programming technologies and paradigms, which can help engineers with their everyday job.