Hey Coders👩💻👨💻!" In this blog post, we're going to explore how Redux Toolkit can simplify state management in your React applications."
🎇Introduction:
In the world of React development, managing state can often feel chaotic. Enter Redux Toolkit, your reliable helper for organized and scalable state management. Think of it as an assistant who not only fetches your data but also keeps everything tidy, handling your requests smoothly.
Why Use Redux Toolkit?
Imagine you have a React app with lots of components that need to share and update the same data. Managing all these states can get messy. Redux Toolkit comes to the rescue by providing a set of tools and best practices to streamline your Redux code.
Setting Up Redux Toolkit
First things first, let's look at how Redux Toolkit is set up in a React project:
Redux Toolkit Documentation
You can access the official documentation for Redux Toolkit to learn more about its features, API, and usage patterns:
Documentation Link: Redux Toolkit Documentation
Folder Structure
Here’s the updated folder structure:
src/
├── components/
├── pages/
│ ├── counter/
│ │ ├── counter.slice.js
│ ├── table/
│ │ ├── table.slice.js
│ │ ├── table.css
│ │ ├── Table.js
├── store/
│ ├── store.js
├── router/
│ ├── router.js
├── App.js
├── App.css
store/: Contains your Redux store configuration.
pages/: Contains React components for different pages and slices.
router/: Handles routing in your React application.
components/: Reusable UI components.
Creating the Store
Your Redux store is created using
configureStore
from Redux Toolkit instore.js
. It combines multiple slices (reducers) into a single store:// store.js import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "../pages/counter/counter.slice"; import tableReducer from "../pages/table/table.slice"; const store = configureStore({ reducer: { counter: counterReducer, tableData: tableReducer, }, }); export default store;
Here's a breakdown of what's happening:
Importing Dependencies:
import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "../pages/counter/counter.slice"; import tableReducer from "../pages/table/table.slice";
We import
configureStore
from@reduxjs/toolkit
, which is a function provided by Redux Toolkit to create a Redux store with simplified setup.We import
counterReducer
andtableReducer
from their respective locations (counter.slice.js
andtable.slice.js
). These reducers define how specific parts of the application state should be updated in response to actions.
Creating the Redux Store:
const store = configureStore({ reducer: { counter: counterReducer, tableData: tableReducer, }, });
We use
configureStore()
to create a Redux store. This function accepts an object with areducer
property.Inside
reducer
, we define key-value pairs where:counter
is the slice of state managed bycounterReducer
.tableData
is the slice of state managed bytableReducer
.
Each reducer (
counterReducer
andtableReducer
) specifies how the corresponding part of the state should be updated when actions are dispatched.
Exporting the Store:
export default store;
- Finally, we export the configured Redux store (
store
) so that it can be imported and used in other parts of the application, such as components that need to access or update the application state.
- Finally, we export the configured Redux store (
WhyconfigureStore
is used:
configureStore
is used here because it simplifies the setup of a Redux store by automatically configuring Redux Toolkit's recommended middleware, includingredux-thunk
for async logic. It also enables Redux DevTools Extension by default, providing a better development experience.By using
configureStore
, we avoid the boilerplate code that would otherwise be required to set up middleware and other store enhancers manually when using plain Redux.Defining Slices (Reducers)
Slices define how your state can be updated. They include actions and reducers, making it easier to manage immutable state updates:
// counter.slice.js import { createSlice } from "@reduxjs/toolkit"; const counterSlice = createSlice({ name: "counter", initialState: { count: 0, setHidden: false, }, reducers: { handleIncrement: (state) => { state.count++; }, handleDecrement: (state) => { state.count--; }, handleReset: (state) => { state.count = 0; }, handleToggle: (state) => { state.setHidden = !state.setHidden; }, }, }); export const { handleIncrement, handleDecrement, handleToggle, handleReset, } = counterSlice.actions; export default counterSlice.reducer;
Importing Dependencies:
import { createSlice } from "@reduxjs/toolkit";
We import
createSlice
from@reduxjs/toolkit
, a function that helps to create a Redux slice. A slice in Redux is a piece of state with its own reducers and actions.Creating the Slice:
const counterSlice = createSlice({ name: "counter", initialState: { count: 0, setHidden: false, }, reducers: { handleIncrement: (state) => { state.count++; }, handleDecrement: (state) => { state.count--; }, handleReset: (state) => { state.count = 0; }, handleToggle: (state) => { state.setHidden = !state.setHidden; }, }, });
createSlice
is used to create a slice named"counter"
with initial state defined ininitialState
.Inside
reducers
, we define functions (handleIncrement
,handleDecrement
,handleReset
,handleToggle
) that describe how the state should be updated in response to specific actions.Each reducer function receives the current
state
as a parameter and can modify it directly becausecreateSlice
uses Immer internally to allow mutable updates.
Exporting Actions and Reducer:
export const { handleIncrement, handleDecrement, handleToggle, handleReset, } = counterSlice.actions; export default counterSlice.reducer;
counterSlice.actions
contains the action creators (handleIncrement
,handleDecrement
,handleToggle
,handleReset
) that are automatically generated bycreateSlice
.counterSlice.reducer
exports the reducer function that manages how the state changes in response to dispatched actions.Connecting Components
Use
useSelector
to read from the Redux store anduseDispatch
to dispatch actions. Here’s an example fromCounter
component:
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import {
handleIncrement,
handleDecrement,
handleReset,
handleToggle,
} from "./counter.slice";
const Counter = () => {
const data = useSelector((state) => state.counter);
const dispatch = useDispatch();
return (
<div>
{data.setHidden && (
<>
<button onClick={() => dispatch(handleIncrement())}>+</button>
<button onClick={() => dispatch(handleDecrement())}>-</button>
<button onClick={() => dispatch(handleReset())}>reset</button>
</>
)}
<button onClick={() => dispatch(handleToggle())}>toggle</button>
<div>{data.count}</div>
</div>
);
};
export default Counter;
Explanation:
Importing Redux Hooks:
import { useDispatch, useSelector } from "react-redux";
- We import
useSelector
anduseDispatch
hooks fromreact-redux
. These hooks are used to access the Redux store state and dispatch actions, respectively.
- We import
Using
useSelector
:const data = useSelector((state) => state.counter);
useSelector
is called with a selector function(state) => state.counter
. It selects thecounter
slice from the Redux store state (state.counter
).
Using
useDispatch
:const dispatch = useDispatch();
useDispatch
returns a reference to thedispatch
function from the Redux store. This function is used to dispatch actions to update the Redux state.
Rendering the Component:
return ( <div> {data.setHidden && ( <> <button onClick={() => dispatch(handleIncrement())}>+</button> <button onClick={() => dispatch(handleDecrement())}>-</button> <button onClick={() => dispatch(handleReset())}>reset</button> </> )} <button onClick={() => dispatch(handleToggle())}>toggle</button> <div>{data.count}</div> </div> );
The component renders buttons and displays the
count
from the Redux state.When
data.setHidden
istrue
, it renders buttons for incrementing (+
), decrementing (-
), and resetting (reset
) the count. Each button click dispatches the corresponding action (handleIncrement
,handleDecrement
,handleReset
).The
toggle
button togglesdata.setHidden
state when clicked, dispatching thehandleToggle
action.
Jokes
Now, let’s add some fun to the mix! Remember, learning should be enjoyable too. Imagine your Redux state as a pizza that you can add toppings to (actions) and share slices of (reducers). Don’t worry, we’ll avoid over-topping with too much state!
Let's dive into how we handle API calls and filter data using Redux Toolkit intable.slice.js
.
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { current } from "@reduxjs/toolkit";
export const fetchTableData = createAsyncThunk(
"table-data/getuserList",
async () => {
const response = await axios("https://reqres.in/api/users?page=2");
return response.data;
}
);
const TableSlice = createSlice({
name: "table-data",
initialState: {
userTableData: [],
filterUserData: [],
loading: false,
error: "",
},
extraReducers: (builder) => {
builder
.addCase(fetchTableData.pending, (state, action) => {
state.loading = true;
})
.addCase(fetchTableData.fulfilled, (state, action) => {
state.userTableData = [...action.payload.data];
state.filterUserData = [...action.payload.data];
state.loading = false;
state.error = "";
})
.addCase(fetchTableData.rejected, (state) => {
state.error = "something went wrong";
state.loading = false;
});
},
//in memory
reducers: {
deleteUserData: (state, action) => {
// console.log(action,"hello state")
let data = current(state).filterUserData;
let filterData = data.filter((user) => user.id !== action.payload);
state.filterUserData = [...filterData];
},
onFilterUserData: (state, action) => {
let data = current(state).filterUserData;
let searchText = action.payload;
console.log(searchText);
if (searchText) {
const filtered = data?.filter((user) => {
return (
user.id.toString().includes(searchText) ||
user.first_name.toLowerCase().includes(searchText.toLowerCase()) ||
user.last_name.toLowerCase().includes(searchText.toLowerCase())
);
});
state.filterUserData = [...filtered];
} else {
state.filterUserData = [...state.userTableData];
}
},
updateEmloyeer: (state, action) => {
console.log(current(state), action);
const { id, first_name, last_name } = action.payload;
const index = state.filterUserData.findIndex((emp) => emp.id === id);
if (index !== -1) {
state.filterUserData[index].first_name = first_name;
state.filterUserData[index].last_name = last_name;
}
},
},
});
export const {
editUserData,
deleteUserData,
onFilterUserData,
updateEmloyeer,
} = TableSlice.actions;
export default TableSlice.reducer;
Code Explanation:
Imports:
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import axios from "axios"; import { current } from "@reduxjs/toolkit";
Imports
createAsyncThunk
andcreateSlice
from@reduxjs/toolkit
for setting up async actions and creating slices.Imports
axios
for making HTTP requests.current
is imported for accessing the current state within reducers.
Async Action (
fetchTableData
):export const fetchTableData = createAsyncThunk( "table-data/getuserList", async () => { const response = await axios("https://reqres.in/api/users?page=2"); return response.data; } );
createAsyncThunk
generates an async action creator namedfetchTableData
.It makes an API call to fetch user data from
https://reqres.in/api/users?page=2
.The fetched data (
response.data
) is returned as the payload.
Slice (
TableSlice
):javascriptCopy codeconst TableSlice = createSlice({ name: "table-data", initialState: { userTableData: [], filterUserData: [], loading: false, error: "", }, extraReducers: (builder) => { builder .addCase(fetchTableData.pending, (state, action) => { state.loading = true; }) .addCase(fetchTableData.fulfilled, (state, action) => { state.userTableData = [...action.payload.data]; state.filterUserData = [...action.payload.data]; state.loading = false; state.error = ""; }) .addCase(fetchTableData.rejected, (state) => { state.error = "something went wrong"; state.loading = false; }); }, reducers: { deleteUserData: (state, action) => { let data = current(state).filterUserData; let filterData = data.filter((user) => user.id !== action.payload); state.filterUserData = [...filterData]; }, onFilterUserData: (state, action) => { let data = current(state).filterUserData; let searchText = action.payload; if (searchText) { const filtered = data?.filter((user) => { return ( user.id.toString().includes(searchText) || user.first_name.toLowerCase().includes(searchText.toLowerCase()) || user.last_name.toLowerCase().includes(searchText.toLowerCase()) ); }); state.filterUserData = [...filtered]; } else { state.filterUserData = [...state.userTableData]; } }, updateEmloyeer: (state, action) => { const { id, first_name, last_name } = action.payload; const index = state.filterUserData.findIndex((emp) => emp.id === id); if (index !== -1) { state.filterUserData[index].first_name = first_name; state.filterUserData[index].last_name = last_name; } }, }, });
createSlice
creates a Redux slice named"table-data"
with initial state containinguserTableData
,filterUserData
,loading
, anderror
.extraReducers
handles actions dispatched byfetchTableData
:pending
: Setsloading
totrue
.fulfilled
: UpdatesuserTableData
andfilterUserData
with fetched data, resetsloading
and clearserror
.rejected
: Setserror
message and resetsloading
.
reducers
contains additional synchronous actions:deleteUserData
: Filters out a user based onaction.payload
.onFilterUserData
: FiltersfilterUserData
based onaction.payload
.updateEmloyeer
: Updates user data based onaction.payload
.
Export Actions and Reducer:
export const { deleteUserData, onFilterUserData, updateEmloyeer, } = TableSlice.actions; export default TableSlice.reducer;
Exports action creators (
deleteUserData
,onFilterUserData
,updateEmloyeer
) and the reducer function fromTableSlice
.
Table.js
Now, let's explain the
Table
component which uses Redux for state management and renders data fetched from the API:import React, { useEffect, useState } from "react"; import "./table.css"; import { useDispatch, useSelector } from "react-redux"; import { deleteUserData, fetchTableData, onFilterUserData, updateEmloyeer, } from "./table.slice"; import { useNavigate } from "react-router-dom"; const Table = () => { const tableDetails = useSelector((state) => state.tableData); const [searchInput, setSearchInput] = useState(""); const dispatch = useDispatch(); React.useEffect(() => { dispatch(fetchTableData()); }, []); useEffect(() => { dispatch(onFilterUserData(searchInput)); }, [searchInput]); const navigate = useNavigate(); const handleRowClick = (uid) => { navigate(`/table/${uid}`); }; const handleEdit = (editId, newFirstName, newLastName) => { console.log(editId); const fullName = prompt( "Enter new full name (First Last):", `${newFirstName} ${newLastName}` ); if (fullName !== null) { const [newFirstName, newLastName] = fullName.split(" "); dispatch( updateEmloyeer({ id: editId, first_name: newFirstName, last_name: newLastName, }) ); } }; const handleDelete = (userId) => { console.log("Deleting user with ID:", userId); dispatch(deleteUserData(userId)); }; return ( <div className="table-outside-container"> {tableDetails.loading && <h1>Loding....</h1>} {tableDetails.error && <hi>{tableDetails.error}</hi>} <div class="wrapper"> <div class="search-input"> <input type="text" placeholder="Type to search..." value={searchInput} onChange={(e) => setSearchInput(e.target.value)} /> <div class="icon"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16" > <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0" /> </svg> </div> </div> </div> <section className="table-inside-conatiner"> {tableDetails.filterUserData.length > 0 && ( <table style={{ width: "90%" }}> <tr> <th>Id</th> <th>First Name</th> <th>Last Name</th> <th>Full Details</th> <th>Action</th> </tr> <tbody> {tableDetails?.filterUserData.map((userData, index) => ( <tr key={index}> <td>{userData.id}</td> <td>{userData?.first_name}</td> <td>{userData?.last_name}</td> <td onClick={() => handleRowClick(userData.id)}>Detail</td> <td className="button-conatiner"> <button className="edit" onClick={() => handleEdit( userData.id, userData.first_name, userData.last_name ) } > Edit </button> <button className="delete" onClick={() => handleDelete(userData.id)} > Delete </button> </td> </tr> ))} </tbody> </table> )} {tableDetails?.filterUserData === 0 && ( <p>No matching records found.</p> )} </section> </div> ); }; export default Table;
Code Explanation:
Imports and Setup:
javascriptCopy codeimport React, { useEffect, useState } from "react"; import "./table.css"; import { useDispatch, useSelector } from "react-redux"; import { deleteUserData, fetchTableData, onFilterUserData, updateEmloyeer, } from "./table.slice"; import { useNavigate } from "react-router-dom";
Imports necessary dependencies including React hooks (
useEffect
,useState
), Redux hooks (useDispatch
,useSelector
), and actions fromtable.slice.js
.Imports
useNavigate
for programmatic navigation.
Component (
Table
):javascriptCopy codeconst Table = () => { const tableDetails = useSelector((state) => state.tableData); const [searchInput, setSearchInput] = useState(""); const dispatch = useDispatch(); React.useEffect(() => { dispatch(fetchTableData()); }, []); useEffect(() => { dispatch(onFilterUserData(searchInput)); }, [searchInput]); const navigate = useNavigate(); const handleRowClick = (uid) => { navigate(`/table/${uid}`); }; const handleEdit = (editId, newFirstName, newLastName) => { const fullName = prompt( "Enter new full name (First Last):", `${newFirstName} ${newLastName}` ); if (fullName !== null) { const [newFirstName, newLastName] = fullName.split(" "); dispatch( updateEmloyeer({ id: editId, first_name: newFirstName, last_name: newLastName, }) ); } }; const handleDelete = (userId) => { dispatch(deleteUserData(userId)); }; return ( <div className="table-outside-container"> {tableDetails.loading && <h1>Loading....</h1>} {tableDetails.error && <h1>{tableDetails.error}</h1>} <div className="wrapper"> <div className="search-input"> <input type="text" placeholder="Type to search..." value={searchInput} onChange={(e) => setSearchInput(e.target.value)} /> <div className="icon"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" className="bi bi-search" viewBox="0 0 16 16" > <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0" /> </svg> </div> </div> </div> <section className="table-inside-container"> {tableDetails.filterUserData.length > 0 && ( <table style={{ width: "90%" }}> <thead> <tr> <th>Id</th> <th>First Name</th> <th>Last Name</th> <th>Full Details</th> <th>Action</th> </tr> </thead> <tbody> {tableDetails.filterUserData.map((userData, index) => ( <tr key={index}> <td>{userData.id}</td> <td>{userData.first_name}</td> <td>{userData.last_name}</td> <td onClick={() => handleRowClick(userData.id)}>Detail</td> <td className="button-container"> <button className="edit" onClick={() => handleEdit( userData.id, userData.first_name, userData.last_name ) } > Edit </button> <button className="delete" onClick={() => handleDelete(userData.id)} > Delete </button> </td> </tr> ))} </tbody> </table> )} {tableDetails.filterUserData.length === 0 && ( <p>No matching records found.</p> )} </section> </div> ); };
Table
component renders a table with user data fetched from Redux state (tableDetails.filterUserData
).Uses
useState
to managesearchInput
for filtering users based on search input.Uses
useDispatch
to dispatch actions (fetchTableData
,onFilterUserData
,updateEmloyeer
,deleteUserData
).Uses
useEffect
to fetch initial data (fetchTableData
) when the component mounts and to update filtered data (onFilterUserData
) whensearchInput
changes.Defines functions
handleRowClick
for navigating to detailed user page,handleEdit
for editing user data, andhandleDelete
for deleting a user.Renders table headers (
Id
,First Name
,Last Name
,Full Details
,Action
) and rows dynamically fromtableDetails.filterUserData
.Handles loading state (
tableDetails.loading
) and error state (tableDetails.error
) for better user experience.
Conclusion:
Armed with Redux Toolkit, your React apps gain robustness and maintainability. It streamlines complex state management into a delightful experience, leaving you more time to browse cat memes on a lazy Sunday—because who doesn't love a good laugh between code commits?
🧿Final Words:
Whether you're diving into Redux Toolkit for the first time or honing your skills, may your state always be manageable, your components reusable, and your Redux journeys as smooth as a well-oiled machine. Happy coding, and remember to share your Redux Toolkit tales in the comments below—let's keep the laughter and learning going! 🚀🎉