Introduction to Redux toolkit
What is Redux toolkit ?
Redux Toolkit is an official set of tools that makes using Redux easier. It reduces the extra code you usually have to write and comes with built-in features like createSlice
, createAsyncThunk
, and configureStore
to help manage your app's state more efficiently.
Why use Redux Toolkit over plain Redux ?
Redux Toolkit is better than plain Redux because it makes state management easier by reducing the amount of code you need to write. It simplifies common tasks like setting up the store, creating actions, and handling async operations, making development faster and less error-prone.
Problems with traditional Redux
Boilerplate code: You need to write a lot of repetitive code, like action types, action creators, and reducers.
Complexity: Managing state and async actions can get complicated, especially for large applications.
Manual setup: You have to configure the store, middleware, and DevTools yourself, which can be tedious and error-prone.
These issues make traditional Redux more time-consuming and harder to manage.
Installation and Setup
To install Redux Toolkit and react-redux
, run the following command in your project:
npm install @reduxjs/toolkit react-redux
This installs both the Redux Toolkit for managing state and React-Redux for connecting Redux to your React components.
Setting up Redux Toolkit in a React project.
Create a Redux store:
Create a new file, e.g.,
store.js
, and set up the store usingconfigureStore
:import { configureStore } from "@reduxjs/toolkit"; import authSlice from "../features/authSlice"; import userSlice from "../features/userSlice"; export const store = configureStore({ reducer: { auth: authSlice, users: userSlice, }, }); export default store;
Create slices:
As You can see I have two different slices which I have allocated in the store :
i. authSlice
ii. userSlice
Let’s define them
1. AuthSlice.js
// src/features/authSlice.js import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { post } from "../services/apiService"; // Adjust import path as needed import { showLoading, hideLoading } from "./loadingSlice"; import { showAlert } from "./alertSlice"; import config from "../config/config"; // Define initial state const initialState = { data: null, status: "idle", error: null, }; // Create an async thunk for login export const login = createAsyncThunk( // "auth/login", "api/v1/authenticate", async (credentials, { dispatch }) => { try { dispatch(showLoading()); const response = await post( config.apiEndpoints.authenticate, credentials ); dispatch(showAlert({ message: "Login successful", severity: "success" })); return response.data; } catch (error) { const errorMessage = error.response?.data?.message || "Login failed"; dispatch(showAlert({ message: errorMessage, severity: "error" })); throw error; } finally { dispatch(hideLoading()); } } ); // Create the slice const authSlice = createSlice({ name: "auth", initialState, reducers: { logout: (state) => { state.data = null; state.status = "idle"; }, }, extraReducers: (builder) => { builder .addCase(login.pending, (state) => { state.status = "loading"; }) .addCase(login.fulfilled, (state, action) => { state.status = "succeeded"; state.data = action.payload; }) .addCase(login.rejected, (state, action) => { state.status = "failed"; state.error = action.error.message; }); }, }); // Export actions and reducer export const { logout } = authSlice.actions; export default authSlice.reducer;
2.UserSlice.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { get, post, put } from "../services/apiService";
import { showLoading, hideLoading } from "./loadingSlice";
import { showAlert } from "./alertSlice";
import config from "../config/config";
const initialState = {
usersList: [],
rolesList: [],
userCreated: false,
};
// Create an async thunk for fetching all users
export const getAllUser = createAsyncThunk(
"users/getAllUser",
async (payload, { dispatch }) => {
try {
dispatch(showLoading());
const response = await post(config.apiEndpoints.getAllUsers, payload);
if (response && response.status === 200 && response.data) {
dispatch(
showAlert({
message: response.message,
severity: "success",
})
);
return response.data;
} else {
dispatch(
showAlert({
message: response.message,
severity: "error",
})
);
}
} catch (error) {
dispatch(
showAlert({
message: error.response?.data?.message || "Failed to fetch users",
severity: "error",
})
);
throw error;
} finally {
dispatch(hideLoading());
}
}
);
// get all available roles
export const getAllRoles = createAsyncThunk(
"api/roles/getAll",
async (payload, { dispatch }) => {
try {
dispatch(showLoading());
const response = await post(config.apiEndpoints.getAllRoles, payload);
dispatch(hideLoading());
return response;
} catch (error) {
dispatch(hideLoading());
throw error;
}
}
);
// create User
export const createUser = createAsyncThunk(
"api/users/create/user",
async (payload, { dispatch }) => {
try {
dispatch(showLoading());
const response = await post(config.apiEndpoints.createUser, payload);
dispatch(hideLoading());
return response;
} catch (error) {
dispatch(hideLoading());
dispatch(
// showAlert({ message: "User creation failed ", severity: "error" })
showAlert({
message: error.response?.data?.message || "User creation failed ",
severity: "error",
})
);
throw error;
}
}
);
// update User
export const updateUser = createAsyncThunk(
"api/users/update/user",
async (payload, { dispatch }) => {
try {
dispatch(showLoading());
const response = await put(config.apiEndpoints.updateUser, payload);
dispatch(hideLoading());
return response;
} catch (error) {
dispatch(hideLoading());
dispatch(
// showAlert({ message: "User creation failed ", severity: "error" })
showAlert({
message: error.response?.data?.message || "User updation failed ",
severity: "error",
})
);
throw error;
}
}
);
const usersSlice = createSlice({
name: "users",
initialState,
reducers: {
resetUserState(state) {
state.userCreated = false;
},
},
extraReducers: (builder) => {
builder
.addCase(getAllUser.fulfilled, (state, action) => {
state.usersList = action.payload;
})
.addCase(getAllRoles.fulfilled, (state, action) => {
state.rolesList = action.payload;
})
.addCase(createUser.fulfilled, (state, action) => {
state.userCreated = true;
})
.addCase(updateUser.fulfilled, (state, action) => {
state.userCreated = true;
})
.addMatcher(
(action) =>
[createUser.rejected, updateUser.rejected].includes(action.type),
(state) => {
state.userCreated = false;
}
);
},
});
export const { resetUserState } = usersSlice.actions;
export default usersSlice.reducer;
In the authslice I am authenticating a particular user and in the userslice I am creating ,updating and fetching all users.
. Provide the store to your app:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './app/App';
import { Provider } from 'react-redux';
import store from './app/store';
import Loading from './components/Loading';
import AlertBar from './components/Alertbar';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
<Loading />
<AlertBar />
</Provider>
);
Use Redux state in your components:
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import {
TextField,
Autocomplete,
Button,
Paper,
Container,
FormControlLabel,
Switch,
} from "@mui/material";
import Grid from "@mui/material/Grid2";
import Navbar from "../../components/Navbar";
import Header from "../../components/Header";
import {
getAllRoles,
createUser,
updateUser,
resetUserState,
} from "../../features/userSlice";
import { showAlert } from "../../features/alertSlice";
import CommonForm from "../../components/CommonForm";
import {
createUserFormControls,
dummyRolesList,
} from "../../static/FormControls";
// const initialErrors = {
// userName: false,
// fullName: false,
// email: false,
// role: false,
// // department: false,
// };
const initialErrors = {
userName: false,
email: false,
phoneNumber: false,
address: false,
role: false,
};
const initialFormData = {
userId: null,
userName: "",
email: "",
phoneNumber: "",
address: "",
role: [],
// department: null,
// isActive: true,
};
const CreateUser = () => {
const navigate = useNavigate();
const location = useLocation();
console.log("location", location);
const [updatingUser, setUpdatingUser] = useState(false);
const dispatch = useDispatch();
const { rolesList } = useSelector(
(state) => state?.users?.rolesList?.data || []
);
const userCreated = useSelector((state) => state?.users?.userCreated);
const [formData, setFormData] = useState(initialFormData);
console.log("formData", formData);
const [errors, setErrors] = useState(initialErrors);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
if (value.trim() !== "") {
setErrors({ ...errors, [name]: false });
} else {
setErrors({ ...errors, [name]: true });
}
};
const handleAutocompleteChange = (name, value) => {
setFormData({ ...formData, [name]: value || [] });
if (value && value.length > 0) {
setErrors({ ...errors, [name]: false });
} else {
setErrors({ ...errors, [name]: true });
}
};
const handleSubmit = async (e) => {
e.preventDefault();
if (formData.phoneNumber.length != 10) {
dispatch(
showAlert({
message: "Phone number should be 10 digits",
severity: "error",
})
);
}
if (
formData.userName &&
formData.email &&
formData.phoneNumber &&
formData.role.length > 0
) {
const payload = {
id: formData?.userId || null,
username: formData.userName,
email: formData.email,
phoneNumber: formData.phoneNumber,
address: formData.address,
roleIds: formData.role.map((role) => role.id),
roleNames: formData.role.map((role) => role.name),
};
if (updatingUser) {
dispatch(updateUser(payload));
} else {
dispatch(createUser(payload));
}
} else {
if (formData.role.length === 0) {
setErrors({ ...errors, role: true });
}
}
};
const handleInputBlur = (e) => {
const { name } = e.target;
setFormData((prevData) => {
const trimmedValue = prevData[name]?.trim();
if (trimmedValue !== prevData[name]) {
return {
...prevData,
[name]: trimmedValue,
};
}
return prevData;
});
};
useEffect(() => {
document.title = "ATMS- Create User";
return () => {
document.title = "ATMS";
};
//eslint-disable-next-line
}, []);
useEffect(() => {
if (userCreated) {
dispatch(resetUserState());
setTimeout(() => {
navigate(-1);
}, 1000);
}
}, [userCreated]);
useEffect(() => {
const userDto = sessionStorage.getItem("token");
if (userDto) {
dispatch(
getAllRoles({
pageNumber: 0,
pageSize: 10,
})
);
}
}, [navigate, dispatch]);
useEffect(() => {
if (location && location.state) {
setUpdatingUser(true);
const { id, username, email, phoneNumber, address, roleNames } =
location.state;
console.log("roleNames", roleNames);
setFormData({
userId: id,
userName: username || "",
email: email || "",
phoneNumber: phoneNumber || "",
address: address || "",
role:
dummyRolesList.filter((val) => roleNames.includes(val.name)) || [],
});
}
}, [location]);
return (
<>
<Navbar />
<Paper sx={{ padding: "1rem 3rem" }} elevation={0}>
<div className="main-container">
<Grid container justifyContent="space-between" marginBottom={5}>
<Header
gridSize={12}
headerText={updatingUser ? "Update User" : "Create User"}
arrow={true}
/>
</Grid>
<CommonForm
action={handleSubmit}
formControls={createUserFormControls}
formData={formData}
setFormData={setFormData}
handleInputChange={handleInputChange}
handleAutocompleteChange={handleAutocompleteChange}
errors={errors}
setErrors={setErrors}
handleInputBlur={handleInputBlur}
updatingUser={updatingUser}
// options={dummyRolesList}
/>
</div>
</Paper>
</>
);
};
export default React.memo(CreateUser);