Unlock the Power of Redux Toolkit: Exploring State Management in React through a Simple Counter App
Welcome to our blog post where we dive into the fascinating world of state management in React using Redux Toolkit. If you're a React developer looking to level up your skills and gain a deeper understanding of how to effectively manage application state, you've come to the right place.
In this article, we'll take you on a step-by-step journey as we build a simple counter application. However, don't let the simplicity of the app fool you. We'll be harnessing the power of Redux Toolkit to demonstrate how it can simplify the process of managing state and enhance your development workflow.
Whether you're new to Redux or already familiar with it, Redux Toolkit provides a modern and opinionated approach to state management, making it easier and more efficient to work with Redux in your React projects. We'll explore the key features and benefits it offers, such as the simplified Redux store setup, built-in utilities, and intuitive syntax.
By the end of this tutorial, you'll have a solid understanding of how to leverage Redux Toolkit to handle state in your React applications effectively. So, let's roll up our sleeves and embark on this exciting journey of mastering state management with Redux Toolkit in React through our simple yet powerful counter app. Let's get started!
Tutorial
Create a new react app using the following command:
npx create-react-app redux-counter
Open the newly created app in VS Code editor. Remove the boilerplate files and the test files from the source folder, such that we’re only left with App.js, index.js and index.css files.
Next, we will install the redux toolkit with the following command:
npm install @reduxjs/toolkit react-redux
After redux is installed, we will create an ‘app’ folder in the source folder. Within the app folder, we will create a store.js file which will behave as the container for the JavaScript app. It stores the whole state of the app in a mutable object tree.
Next, we will write some introductory logic in store.js
file:
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {
// will hold the reducers we will create
}
})
Next, we will import the store and wrap our app in redux provider in the index.js
file:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { store } from "./app/store";
import { Provider } from "react-redux";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
The above code sets up and renders a React application using ReactDOM. Here's an explanation of the code:
import { store } from "./app/store";
: This line imports a Redux store from a file namedstore.js
orstore.jsx
. The Redux store is used for managing the application state.import { Provider } from "react-redux";
: This line imports theProvider
component from thereact-redux
package. TheProvider
component is used to provide the Redux store to the entire application.<Provider store={store}>
: This component wraps the entire application and provides the Redux store to all the components within it. It takes thestore
as a prop.
Overall, this code sets up the React application, connects it to the Redux store using the Provider
component, and renders the App
component into the root element.
Concept of Slice in Redux
In Redux, a slice refers to a portion of the global state managed by the Redux store. It represents a specific section of the state tree that is isolated and managed by a single reducer function. Slices allow you to divide the state into smaller, more manageable pieces, each with its own reducer, actions, and selectors.
A slice typically consists of three main parts:
- Reducer: A reducer is a function that specifies how the state of a slice should be updated in response to dispatched actions. It takes the current state and an action as parameters and returns the new state based on the action type and payload.
- Actions: Actions are plain JavaScript objects that describe changes to the state. They typically have a
type
property that specifies the type of action and may include additional data in thepayload
property. Actions are dispatched to the store to trigger state updates. - Selectors: Selectors are functions that provide an interface to access specific data from the state. They allow you to extract data from the slice's state in a structured and reusable way. Selectors can compute derived data, perform transformations, or filter data before returning it.
By dividing the state into slices, each slice can have its own reducer, actions, and selectors, making it easier to manage and reason about specific parts of the state. Slices can also be combined to create a larger, global state tree using Redux's combineReducers
function.
Overall, slices provide a modular and organized way to structure the state and logic in a Redux application, making it more maintainable and scalable.
Let's say you have a simple e-commerce application that manages products and shopping cart functionality. You can define two slices: productSlice
and cartSlice
.
-
Product Slice:
- Reducer: Manages the state related to products, such as an array of products and their details.
- Actions: Defines actions like
fetchProducts
,addProduct
, andupdateProduct
to update the product state. - Selectors: Provides selectors like
selectProducts
,selectProductById
, andselectProductCount
to retrieve specific product data from the state.
Cart Slice:
- Reducer: Manages the state related to the shopping cart, such as the items in the cart and the total price.
- Actions: Defines actions like
addItem
,removeItem
, andclearCart
to update the cart state. - Selectors: Provides selectors like
selectCartItems
,selectCartTotal
, andselectCartItemCount
to retrieve cart-related data from the state.
Back to Counter Application
First, we will create a ‘features’ folder within which we will add a ‘counter’ folder. Within the counter folder, we will first create a new counterSlice.js file with the following logic:
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
count: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
},
});
export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;
The above code demonstrates the usage of Redux Toolkit's createSlice
function to create a counter slice. Let's break down the code:
createSlice
function is imported from the@reduxjs/toolkit
package.- An
initialState
object is defined with a single propertycount
set to 0. This represents the initial state of the counter slice. - The
createSlice
function is invoked with an object that contains the following properties:name
: Specifies the name of the slice, which is "counter" in this case.initialState
: The initial state object defined earlier.reducers
: An object containing reducer functions that will handle state updates. In this example, two reducer functions are defined:increment
: Increments thecount
property by 1 when invoked.decrement
: Decrements thecount
property by 1 when invoked.
- The
createSlice
function returns an object with several properties, including theactions
property that contains the generated action creators based on the defined reducers. - The
increment
anddecrement
action creators are extracted from theactions
property using destructuring. - The
counterSlice.reducer
property represents the generated reducer function for the counter slice. - The
counterSlice.reducer
is exported as the default export of the module.
By using this code, you can dispatch the increment
and decrement
actions to update the state of the counter slice and access the current count value using the Redux store.
Next, we will import the counter reducer in store.js
file like this:
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
// will hold the reducers we will create
counter: counterReducer,
},
});
This reducer is now available to the entire app through the provider we wrote in index.js
file earlier.
Next we will create a component file called Counter.js
in the counter folder with the following logic:
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement } from "./counterSlice";
const Counter = () => {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
return (
<section>
<p>{count}</p>
<div>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
</section>
);
};
export default Counter;
The above code demonstrates the usage of useSelector
and useDispatch
hooks from the react-redux
library to connect a component to the Redux store and dispatch actions. Let's break down the code:
- The
useSelector
anduseDispatch
hooks are imported from thereact-redux
package. - The
increment
anddecrement
action creators are imported from the./counterSlice
module. - The
Counter
component is defined. - Within the
Counter
component, theuseSelector
hook is used to select a value from the Redux store. It takes a callback function as an argument that receives the entire Redux state and returns the specific value to be selected. In this case, it selects thecount
property from thecounter
slice in the Redux store. - The
useDispatch
hook is used to get a reference to thedispatch
function provided by the Redux store. This allows the component to dispatch actions to update the state. - The
count
value obtained from the Redux store usinguseSelector
is rendered within a<p>
element. - Two buttons are rendered, one for incrementing the counter and the other for decrementing it.
- The
onClick
event handlers of the buttons call thedispatch
function and pass in the respective action creators (increment
anddecrement
). This triggers the dispatch of the corresponding actions to update the state. - The
Counter
component is exported as the default export of the module.
By using this code, the Counter
component can access the count
value from the Redux store and dispatch actions to increment or decrement it. The component will automatically update whenever the count
value in the Redux store changes.
Next, we will add two actions which will allow us to reset the counter and to increment by a certain amount we specify. First, we’ll add the functions in the counterSlice.js file in addition to the previous ones:
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
count: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.count += 1;
},
decrement: (state) => {
state.count -= 1;
},
reset: (state) => {
state.count = 0;
},
incrementByAmount: (state, action) => {
state.count += action.payload;
},
},
});
export const { increment, decrement, reset, incrementByAmount } =
counterSlice.actions;
export default counterSlice.reducer;
The above code defines a Redux slice using the createSlice
function from the @reduxjs/toolkit
package. Let's break it down:
- The initial state of the counter is defined as an object with a
count
property initialized to 0. createSlice
is invoked with an object containing configuration options:name
: Specifies the name of the slice, which is "counter" in this case.initialState
: The initial state object defined earlier.reducers
: An object containing the reducer functions for updating the state.
- Within the
reducers
object, there are four reducer functions defined:increment
: Increments thecount
value by 1.decrement
: Decrements thecount
value by 1.reset
: Resets thecount
value to 0.incrementByAmount
: Increments thecount
value by the payload amount provided in the action.
- The
counterSlice.actions
object contains all the generated action creators based on the reducer functions. Destructuring assignment is used to extractincrement
,decrement
,reset
, andincrementByAmount
fromcounterSlice.actions
. - The
counterSlice.reducer
represents the reducer function generated bycreateSlice
. It handles the state updates based on the dispatched actions. - The slice's exports include the action creators and the reducer function, making them available for use in other parts of the application.
By utilizing this code, you can easily manage the state related to the counter in your Redux store. The provided reducer functions can be dispatched to update the state and perform actions such as incrementing, decrementing, resetting, or incrementing by a specific amount.
Next, we will add these actions to the Counter.js component in continuation to what is added previously:
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, reset, incrementByAmount } from "./counterSlice";
import { useState } from "react";
const Counter = () => {
const count = useSelector((state) => state.counter.count);
const dispatch = useDispatch();
const [incrementAmount, setIncrementAmount] = useState(0);
const addValue = Number(incrementAmount) || 0;
const resetAll = () => {
setIncrementAmount(0);
dispatch(reset());
};
return (
<section>
<p>{count}</p>
<div>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
<input
type="text"
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<div>
<button onClick={() => dispatch(incrementByAmount(addValue))}>
Add Amount
</button>
<button onClick={resetAll}>Reset</button>
</div>
</section>
);
};
export default Counter;
Let's go through the code:
- The component defines a local state variable
incrementAmount
using theuseState
hook. It is initialized with a default value of 0. - The
addValue
variable is assigned the numeric value ofincrementAmount
usingNumber(incrementAmount)
. IfincrementAmount
is not a valid number, it defaults to 0. - The
resetAll
function is defined, which resets theincrementAmount
to 0 and dispatches thereset
action usingdispatch(reset())
. - An
<input>
element is rendered to allow the user to input a value to increment the count by. Thevalue
attribute is bound to theincrementAmount
state variable, and theonChange
event updates theincrementAmount
state with the entered value. - Two buttons are rendered to add the specified
incrementAmount
to the count usingdispatch(incrementByAmount(addValue))
and to call theresetAll
function when clicked.
The Counter
component combines the Redux state management features with local component state to provide a UI for incrementing, decrementing, resetting, and incrementing by a specified amount for the count
value in the Redux store's counter
slice.
Finally we will add the styling or we could’ve added them in the beginning also in index.css
file:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
font-size: 5rem;
display: grid;
place-content: center;
background-color: cadetblue;
font-family: monospace;
}
input,
button {
font: inherit;
padding: 0.5rem;
border-radius: 1rem;
outline: none;
border: 0.1rem solid darkslategray;
}
section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 350px;
}
p {
font-size: 10rem;
padding: 3rem;
background-color: darkslategray;
color: antiquewhite;
border-radius: 2rem;
}
input {
margin-top: 3rem;
text-align: center;
max-width: 100%;
background-color: antiquewhite;
box-shadow: inset 0.25rem 0.25rem darkslategray;
color: darkslategray;
}
button {
font-size: 2rem;
margin: 0.5em 0 0.5em 0.5em;
min-width: 3em;
padding: 1rem;
box-shadow: 0.25rem 0.25rem darkslategray;
background-color: antiquewhite;
color: darkslategray;
}
button:hover,
button:active {
background-color: darkslategray;
color: antiquewhite;
}
button:first-child {
margin-left: 0;
}
Conclusion
In conclusion, we hope this blog post has provided you with a comprehensive introduction to state management in React using Redux Toolkit. Throughout this journey, we explored the creation of a simple counter application and witnessed firsthand how Redux Toolkit simplifies the process of managing state in React projects.
By leveraging Redux Toolkit's streamlined setup, intuitive syntax, and built-in utilities, we've witnessed the power of efficient state management. Redux Toolkit not only reduces boilerplate code but also improves developer productivity and readability.
Remember, Redux Toolkit is not just limited to counter applications; it can be applied to complex projects with ease. The concepts and techniques covered in this tutorial serve as a solid foundation for tackling more sophisticated state management challenges in your future endeavors.
As you continue to expand your React skills, we encourage you to explore further the capabilities of Redux Toolkit and experiment with its various features. Dive into the Redux Toolkit documentation, explore advanced use cases, and integrate it into your existing or upcoming projects to witness its true potential.
By mastering state management with Redux Toolkit, you'll empower yourself to build robust, scalable, and maintainable React applications. So go ahead, put your newfound knowledge to use, and embark on your journey towards becoming a state management expert.
Thank you for joining us on this exciting adventure. We hope you found this blog post informative and insightful. Happy coding!