In building a big application using React.js, in order to maintain, properly manage and access different state changes across components, developers tend to use state management libraries like React Redux, Recoil, Jotai etc. Most of React.js applications are using React Redux. But having Redux in your React.js brings some concerns like: "Configuring the store is too complicated", "many boilerplates" and "add a lot of packages to get Redux to do anything useful". If you're new in Redux, it would need you to take more time to spend learning the state management. People behind React Redux are aware of this. That's why they introduced a new option to handle your state management with ease. This is Redux Toolkit.
On their site, they described it as their official, batteries-included toolset for efficient Redux development. And it's intended to be the standard way to write Redux logic and they definitely recommend this to use.
Simple - Provides good defaults for store setup out of the box, and includes the most commonly used Redux addons built-in.
Opinionated - Provides good defaults for store setup out of the box, and includes the most commonly used Redux addons built-in.
Powerful - Takes inspiration from libraries like Immer and Autodux to let you write "mutative" immutable update logic, and even create entire "slices" of state automatically.
Effective - Lets you focus on the core logic your app needs, so you can do more work with less code.
sourceAlright, after introducing you about Redux Toolkit, I'll now proceed to setting up and usage of our redux toolkit in our component.
1. Including or Adding Redux Toolkit in the project.
# Redux + Plain JS template
npx create-react-app my-app --template redux
# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript
Install the redux toolkit package NPM: npm install @reduxjs/toolkit
or YARN:yarn add @reduxjs/toolkit
Optionally, you can install other packages like axios because in our example application, I set up the axios and other packages there for UI.
A "Slice" in redux is a collection of redux reducer logic and actions. In our example, we created the employeeSlice.ts
and it's called the API methods.
import { createSlice } from "@reduxjs/toolkit";
import { addEmployee, deleteEmployee, getEmployees, updateEmployee } from "./employeeApi";
export const employeeSlice = createSlice({
name: "employee",
initialState: {
list: {
isLoading: false,
status: "",
values: []
},
save: {
isSaving: false,
isDeleting: false
}
},
reducers: {
clearSuccessMessage: (state, payload) => {
// TODO: Update state to clear success message
}
},
extraReducers: {
[getEmployees.pending.type]: (state, action) => {
state.list.status = "pending"
state.list.isLoading = true
},
[getEmployees.fulfilled.type]: (state, { payload }) => {
state.list.status = "success"
state.list.values = payload
state.list.isLoading = false
},
[getEmployees.rejected.type]: (state, action) => {
state.list.status = "failed"
state.list.isLoading = false
},
[addEmployee.pending.type]: (state, action) => {
state.save.isSaving = true
},
[addEmployee.fulfilled.type]: (state, action) => {
state.save.isSaving = false
},
[addEmployee.rejected.type]: (state, action) => {
state.save.isSaving = false
},
[updateEmployee.pending.type]: (state, action) => {
state.save.isSaving = true
},
[updateEmployee.fulfilled.type]: (state, action) => {
state.save.isSaving = false
},
[updateEmployee.rejected.type]: (state, action) => {
state.save.isSaving = false
},
[deleteEmployee.pending.type]: (state, action) => {
state.save.isDeleting = true
},
[deleteEmployee.fulfilled.type]: (state, action) => {
state.save.isDeleting = false
},
[deleteEmployee.rejected.type]: (state, action) => {
state.save.isDeleting = false
}
}
})
export default employeeSlice.reducer
The store.ts is where we configure and register our store and the slices that we need throughout the application.
import { configureStore } from "@reduxjs/toolkit";
import { useDispatch } from "react-redux";
import employeeSlice from "./features/Employee/employeeSlice";
export const store = configureStore({
reducer: {
employee: employeeSlice
},
});
// dispatch does not take types for thunks into account and thus the return type is typed incorrectly. Please use the actual Dispatch type from the store as decsribed in the documentation. Ref: https://stackoverflow.com/questions/63811401/property-then-does-not-exist-on-type-asyncthunkaction-redux-toolkit
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
On the codes above, this is where we register our reducer. In my example, we're connecting to a web API endpoints and it has endpoints for getting basic employee details.
In the index.tsx
, we add the components Provider
as the parent component of our App
component. This way, we can access the store throughout the app components.
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
For this example, I added some UI design for our single component. In real world scenario, we put the functionalities in separate components, but for the purpose of this blog, we put them into one.
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { IEmployee, IEmployeeList } from "../../models/employee";
import { RootState, useAppDispatch } from "../../store";
import {
getEmployees,
addEmployee,
updateEmployee,
deleteEmployee,
} from "./employeeApi";
import moment from "moment";
import { Input, Checkbox, Button } from "../../components";
import { toast, ToastContainer } from "react-toastify";
export const Employee: React.FC = () => {
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(getEmployees());
}, [dispatch]);
const employeeList = useSelector(
(state: RootState) => state.employee.list.values
);
const isLoadingTable = useSelector(
(state: RootState) => state.employee.list.isLoading
);
const isSaving = useSelector(
(state: RootState) => state.employee.save.isSaving
);
const isDeleting = useSelector(
(state: RootState) => state.employee.save.isDeleting
);
const [employee, setEmployee] = useState<IEmployee>({
employeeId: 0,
name: "",
birthday: moment(new Date()).format("YYYY-MM-DD"),
isActive: false,
});
const [showValidation, setShowValidation] = useState<boolean>(false);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, checked } = e.target;
setEmployee((prevState) => ({
...prevState,
[name]: name === "isActive" ? checked : value,
}));
};
const selectEmployee = (d: IEmployee) => {
setShowValidation(false);
setEmployee({
employeeId: d.employeeId,
name: d.name,
isActive: d.isActive,
birthday: moment(d.birthday).format("YYYY-MM-DD"),
});
};
const removeEmployee = (id: number) => {
if (id)
dispatch(deleteEmployee(id))
.unwrap()
.then((response) => {
toast.success(response);
dispatch(getEmployees());
})
.catch((error) => {
toast.error(error);
});
};
const submit = (e: React.SyntheticEvent) => {
e.preventDefault();
if (employee.name === "") {
setShowValidation(true);
return;
}
const action =
employee.employeeId === 0
? addEmployee(employee)
: updateEmployee(employee);
dispatch(action)
.unwrap()
.then((response) => {
toast.success(response);
resetForm();
dispatch(getEmployees());
})
.catch((error) => {
toast.error(error);
});
};
const resetForm = () => {
setEmployee({
employeeId: 0,
name: "",
isActive: false,
birthday: moment(new Date()).format("YYYY-MM-DD"),
});
setShowValidation(false);
};
return (
<>
<div className="form-container">
<h1 className="title">
Employee
<span className="tag is-link">{employeeList?.length}</span>
</h1>
<div className="card">
<div className="card-content">
<div className="content">
<div className="columns">
<div className="column is-4">
<Checkbox
title="Active"
name="isActive"
value={employee.isActive}
inputChange={handleInputChange}
/>
</div>
</div>
<div className="columns">
<div className="column is-4">
<Input
type="text"
title="Name"
name="name"
placeholder="Enter name here"
value={employee.name}
inputChange={handleInputChange}
showValidation={showValidation}
isRequired={true}
/>
</div>
<div className="column is-4">
<Input
type="date"
title="Birthday"
name="birthday"
value={employee.birthday}
inputChange={handleInputChange}
/>
</div>
</div>
<Button
type="is-success"
loading={isSaving}
title="Submit"
onClick={submit}
disabled={isSaving || isDeleting}
/>
{employee.employeeId !== 0 && (
<Button
title="Cancel"
onClick={resetForm}
disabled={isSaving || isDeleting}
/>
)}
<hr />
{isLoadingTable && (
<div className="has-text-centered">Fetching...</div>
)}
<div className="table-container">
<table className="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Active</th>
<th>Birthday</th>
<th></th>
</tr>
</thead>
<tbody>
{employeeList?.map((d: IEmployeeList, index: number) => {
return (
<tr key={index}>
<td>{d.name}</td>
<td>{d.isActive ? "Active" : "Inactive"}</td>
<td>{moment(d.birthday).format("MM/DD/YYYY")}</td>
<td>
<Button
type="is-warning"
title="Edit"
onClick={() => selectEmployee(d)}
disabled={isSaving || isDeleting}
/>
<Button
type="is-danger"
title="Delete"
loading={isDeleting}
onClick={() => removeEmployee(d.employeeId)}
disabled={isSaving || isDeleting}
/>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</div>
<ToastContainer closeOnClick={true} />
</div>
</>
);
};
As you can see in the code above, we can easily access the state store by using useSelector
and if we can call a method that's part of our slice, we can just const dispatch = useAppDispatch();
The full example and source code is available in this repository. https://github.com/deanilvincent/React.JS-ReduxToolkit-Typescript-CRUD-Sample