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 named store.js or store.jsx. The Redux store is used for managing the application state.
  • import { Provider } from "react-redux";: This line imports the Provider component from the react-redux package. The Provider 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 the store 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:

  1. 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.
  2. 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 the payload property. Actions are dispatched to the store to trigger state updates.
  3. 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.

  1. Product Slice:

    • Reducer: Manages the state related to products, such as an array of products and their details.
    • Actions: Defines actions like fetchProducts, addProduct, and updateProduct to update the product state.
    • Selectors: Provides selectors like selectProducts, selectProductById, and selectProductCount to retrieve specific product data from the state.
  2. 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, and clearCart to update the cart state.
  • Selectors: Provides selectors like selectCartItems, selectCartTotal, and selectCartItemCount 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:

  1. createSlice function is imported from the @reduxjs/toolkit package.
  2. An initialState object is defined with a single property count set to 0. This represents the initial state of the counter slice.
  3. 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 the count property by 1 when invoked.
      • decrement: Decrements the count property by 1 when invoked.
  4. The createSlice function returns an object with several properties, including the actions property that contains the generated action creators based on the defined reducers.
  5. The increment and decrement action creators are extracted from the actions property using destructuring.
  6. The counterSlice.reducer property represents the generated reducer function for the counter slice.
  7. 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:

  1. The useSelector and useDispatch hooks are imported from the react-redux package.
  2. The increment and decrement action creators are imported from the ./counterSlice module.
  3. The Counter component is defined.
  4. Within the Counter component, the useSelector 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 the count property from the counter slice in the Redux store.
  5. The useDispatch hook is used to get a reference to the dispatch function provided by the Redux store. This allows the component to dispatch actions to update the state.
  6. The count value obtained from the Redux store using useSelector is rendered within a <p> element.
  7. Two buttons are rendered, one for incrementing the counter and the other for decrementing it.
  8. The onClick event handlers of the buttons call the dispatch function and pass in the respective action creators (increment and decrement). This triggers the dispatch of the corresponding actions to update the state.
  9. 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:

  1. The initial state of the counter is defined as an object with a count property initialized to 0.
  2. 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.
  3. Within the reducers object, there are four reducer functions defined:
    • increment: Increments the count value by 1.
    • decrement: Decrements the count value by 1.
    • reset: Resets the count value to 0.
    • incrementByAmount: Increments the count value by the payload amount provided in the action.
  4. The counterSlice.actions object contains all the generated action creators based on the reducer functions. Destructuring assignment is used to extract increment, decrement, reset, and incrementByAmount from counterSlice.actions.
  5. The counterSlice.reducer represents the reducer function generated by createSlice. It handles the state updates based on the dispatched actions.
  6. 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:

  1. The component defines a local state variable incrementAmount using the useState hook. It is initialized with a default value of 0.
  2. The addValue variable is assigned the numeric value of incrementAmount using Number(incrementAmount). If incrementAmount is not a valid number, it defaults to 0.
  3. The resetAll function is defined, which resets the incrementAmount to 0 and dispatches the reset action using dispatch(reset()).
  4. An <input> element is rendered to allow the user to input a value to increment the count by. The value attribute is bound to the incrementAmount state variable, and the onChange event updates the incrementAmount state with the entered value.
  5. Two buttons are rendered to add the specified incrementAmount to the count using dispatch(incrementByAmount(addValue)) and to call the resetAll 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!

 

Popular Posts

Perform CRUD (Create, Read, Update, Delete) Operations using PHP, MySQL and MAMP : Part 4. My First Angular-15 Ionic-7 App

Visualize Your Data: Showcasing Interactive Charts for Numerical Data using Charts JS Library (Part 23) in Your Angular-15 Ionic-7 App

How to Build a Unit Converter for Your Baking Needs with HTML, CSS & Vanilla JavaScript