Understanding Redux and Redux Toolkit

Understanding Redux and Redux Toolkit

A Step-by-Step Tutorial

ยท

8 min read

Redux, an open-source JavaScript library, plays a pivotal role in managing the state of an application. While React provides a simple way to manage state within components, Redux steps in when the need for centralized state management arises. However, one common criticism has been the amount of boilerplate code required.

Introducing Redux Toolkit โœจ

To address the redundancy of traditional Redux setups, the Redux team introduced the Redux Toolkit. It is a set of tools and conventions designed to simplify working with Redux, making it more approachable and developer-friendly. Redux Toolkit dismiss much of the boilerplate code, allowing developers to focus more on building features rather than configuring state management.

Step 1: Setting Up a Vite Project and Installing Redux Toolkit ๐Ÿ› 

Let's start by creating a new Vite project and installing the necessary dependencies for Redux Toolkit. Open your terminal and execute the following commands:

npx create-vite@latest my-redux-app --template react
cd my-redux-app
npm install @reduxjs/toolkit react-redux

This will set up a new Vite project with React and install the required packages for Redux Toolkit.

Step 2: Creating an Empty Store ๐ŸŽ

Now that our project is set up with Redux Toolkit, the next step is to create an empty Redux store. Open your project in a code editor and locate the entry file (usually src/main.js or src/index.js). Add the following code:

// src/main.js

import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {},
});

In this step, we've imported configureStore from Redux Toolkit and used it to create a basic Redux store. The reducer field is currently empty, and as your application grows, you'll add reducers to manage different parts of the state.

Creating Empty Store

Step 3: Defining Initial State, Reducers, and Actions ๐Ÿงฉ

In Redux Toolkit, understanding the initial state, reducers, and actions is crucial for effective state management. Let's delve into each of these concepts and how they play a role in shaping the behavior of your Redux store.

Initial State in Redux Toolkit ๐Ÿ

In Redux, the initial state represents the starting point of your application's state tree. It defines the structure and default values of your state before any actions are dispatched. In our example, we'll create a simple counter slice with an initial state of 0.

import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {},
});

// This is not allowed in React directly, but it works in Redux due to the immer library.

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

Here, the initialState is set to an object with a property value initialized to 0, reflecting the starting point of our counter. Additionally, it includes a comment about directly modifying the state, which is facilitated by the immer library behind the scenes in Redux Toolkit.

Reducers and Actions ๐Ÿง 

Reducers are functions that specify how the state changes in response to dispatched actions. Actions, on the other hand, are payloads of information that send data from your application to the Redux store. In our example, we'll define increment and decrement actions along with their corresponding reducers.

import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
      // This is not allowed in React directly, but it works in Redux due to the Immer library.
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

Here, we've incorporated the provided code defining the increment and decrement actions along with their reducers. The comments within the code highlight the specific behavior of updating the state directly within reducers, a capability enabled by the immer library.

reducera logic

Exporting Reducers ๐Ÿš—

To make these reducers accessible to our Redux store, we need to export them. This allows us to combine them in the main store configuration.

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export default store;

In this step, we've incorporated our counterSlice reducer into the Redux store. As your application grows, you can create additional slices for different features, keeping your state management organized and modular.

exporting reducers

Current State of counterSlice.js ๐Ÿ“ฆ

Let's take a closer look at the current implementation of counterSlice.js in our Redux setup. This file encapsulates the logic for managing the state related to our counter feature.

import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
      // Directly modifying the state is not allowed in react but it is allowed in redux due to the Immer library which works behind the scene
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

In this snippet, we've defined the counterSlice using the createSlice function from Redux Toolkit. The initialState is set to an object with a property value initialized to 0, representing the starting point of our counter.

current state

Reducers and Actions

The reducers section contains two key functions: increment and decrement. These functions define how the state should change in response to corresponding actions. The increment reducer increases the value property by 1, while the decrement reducer decreases it by 1.

Immer Library in Action ๐ŸŽญ

A notable point is the comment regarding the direct modification of the state within the reducers. While directly updating the state is not allowed in React, it works seamlessly in Redux thanks to the immer library behind the scenes. Immer enables a more straightforward way of updating state by allowing mutable updates in a seemingly immutable environment.

Exporting Reducers and Actions ๐Ÿš€

Finally, we export the increment and decrement actions, as well as the entire reducer. These exports will be utilized in the main store configuration to enable the integration of this counter feature into our Redux store.

This completes the current state of counterSlice.js. As we proceed, we'll explore how to connect these actions with React components and dynamically manage the state of our application.

Step 4: Connecting the Counter Slice to the Redux Store

Now that we've defined our

counterSlice with initial state, reducers, and actions, let's connect it to the Redux store. This step ensures that our application can leverage the centralized state management offered by Redux.

In the main store configuration file, often found at src/app/store.js, import the counterSlice and configure the store:

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export default store;

Here, we import the counterSlice and include its reducer, named counterReducer, in the Redux store configuration under the counter key. This establishes the connection between the counterSlice and the Redux store.

As your application grows, you can follow a similar pattern to connect additional slices, maintaining an organized and scalable state management structure.

conneting to store

Part 5: Usage in React Component ๐ŸŽฌ

Overview

In a React application that incorporates Redux, the useSelector and useDispatch hooks from the react-redux library are crucial for interacting with the Redux store. Let's delve into how each of these hooks works.

useSelector - Reading Data from the Redux Store

The useSelector hook allows components to access and read data from the Redux store. It takes a selector function as an argument, specifying which part of the state to extract.

useDispatch - Accessing the dispatch Function

The useDispatch hook provides a reference to the dispatch function from the Redux store. This function is crucial for dispatching actions, initiating state updates.

Part 5: Usage in a React Component ๐ŸŽฌ

Now, let's illustrate the usage of useSelector and useDispatch in a React component that interacts with a Redux store.

import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from '../features/counter/counterSlice';

const Counter = () => {
  // Reading data from the Redux store
  const counter = useSelector((state) => state.counter.value);

  // Accessing the dispatch function
  const dispatch = useDispatch();

  return (
    <div>
      <h1>Counter: {counter}</h1>
      {/* Dispatching the increment action */}
      <button onClick={() => dispatch(increment())}>Increment</button>
      <button onClick={() => dispatch(decrement())}>Decrement</button>
    </div>
  );
};

export default Counter;

Explanation ๐Ÿ“

  • useSelector: In this example, useSelector reads the counter value from the Redux store. The selector function extracts the value property from the counter slice.

  • useDispatch: The useDispatch hook provides access to the dispatch function, which is then used to dispatch the increment and decrement actions when the corresponding buttons are clicked.

  • Component Rendering: The component renders the current counter value and two buttons. Clicking these buttons triggers the dispatching of the associated actions, leading to state updates in the Redux store.

Part 6: Integration with Existing React Application

Utilizing the Counter Component

Now that we have defined the Counter component using useSelector and useDispatch, let's integrate it into our React application.

import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './app/store';
import Counter from './app/components/Counter';

const container = document.getElementById('root');
const root = createRoot(container);

root.render(
  // <StrictMode>
  <Provider store={store}>
    <Counter />
  </Provider>
  // </StrictMode>
);

Explanation ๐Ÿ“

  • Import Libraries and Components: We import the necessary libraries, including createRoot from "react-dom/client," Provider from "react-redux," and our Redux store (store). Additionally, we import the Counter component that we defined earlier. Ensure you adjust the path to the Counter component based on your project structure.

  • Create a Root Container: Using createRoot and obtaining the root container, we set up the environment for rendering our React application.

  • Render the Counter Component: We render the Counter component within the Redux store Provider. This ensures that the Counter component can access and interact with the state managed by the Redux store.

Conclusion ๐ŸŽ‰

By integrating the Counter component into our React application and providing it with the Redux store through the Provider, we establish a connection between the component and the centralized state management system. Now, the Counter component can dynamically read data from the Redux store using useSelector and dispatch actions using useDispatch.

Source Code ๐Ÿ“

Explore the entire source code for the steps discussed in this tutorial below on Replit. If you encounter any issues or have questions, feel free to reach out to me directly. I'm here to help! Connect with me on:

Replit LinkedIn GitHub

ย