DEV Community

Cover image for The Redux Mystery: Why Does It Feel So Hard?
Saleh Ahmed Mahin
Saleh Ahmed Mahin

Posted on

The Redux Mystery: Why Does It Feel So Hard?

React Redux Toolkit Logo

Redux!

Just hearing the name can make new developers nervous. Why all the "boilerplate"? Why so much setup and so many strange rules just to build a simple counter app? Seriously?

It feels much easier to just use the useState() hook to manage everything. When your project is small, like a basic counter or even a little e-commerce website, useState() is beautiful! Life is simple and happy with hooks.

But the trouble starts when your project gets big. Suddenly, managing dozens of pieces of data and sending information from one deeply nested component to another becomes a huge headache. When one state needs to change in one component, and a different component far away needs that new value, it's not easy to handle that in a manageable and clear way.

This huge communication and state-sharing problem is exactly why a powerful tool like Redux was created.

Prop Drilling Nightmares: When useState Becomes Your Enemy?

Anatomy of basic state managaement

The Scenario:
Look at the diagram. This shows how our app's parts, called components, are connected.

  • The Root is the main component at the top.

  • The app splits into three main sections: Navbar, Cmp-2, and Cmp-3.

  • We have a special piece of information called the user state (like if a user is logged in).

  • The user state is created and set (i.e., given its value, user info, like logged in) on the Signin component.

  • However, our Navbar also needs this user state so it can change—for example, showing "Login" or "Hello, User Name."

The Problem:
The crucial user state is only available far down the component tree, at the Signin component's level.

The Navbar component, which is near the top, needs this state to work correctly.

The Old Solution: Prop Drilling:
To fix this, the usual way is to move the user state to the very top file, the Root component.

Then, we would have to send this state and the function to change it (state function) down, one by one, through every child component until it reaches the ones that need it. This is called "prop drilling."

Prop drilling is a hassle! It makes code hard to read and manage, especially when components in the middle don't even use the state but are just passing it along.

Prop Drilling

This illustrates prop drilling, where the user state is manually passed down through every component (Root → Cmp 1 → Cmp 2, etc.). This is a poor approach for large applications because managing state this way becomes overly complex and messy, forcing intermediate components to handle data they don't need.

Taming Complexity: A Beginner's Guide to Understanding Redux

According to its official documentation, Redux is a JavaScript library designed for "predictable and maintainable global state management." This definition simply means that Redux handles your application's data, or state, on a global level. By keeping the state outside of any single component, we can easily access and use this information from anywhere in the entire application whenever it's needed, completely solving the "prop drilling" problem.

Under the Hood: How Redux Manages Your Global State

Redux state management

The core of Redux is the Store. Think of the Store as a single, central container that holds all of your application's state data. We use a component called a Provider to wrap our entire application around this Store. By doing this, we instantly gain global access to all the states held inside. This allows any component, anywhere in the application, to use the necessary state data without any prop drilling whatsoever.

Following the State: The Predictable Journey of Data in Redux

Redux workflow anatomy

  • View/UI Action: The user interacts with the View / UI (e.g., clicks a button). The View doesn't change the data directly; instead, it tells the system it wants a change.

  • Action Creation & Dispatch: The Vie sends a request to the Actions creator. This creator formats the request into an Action (a plain JavaScript object describing what happened). The Action is then sent forward using dispatch(action).

  • Store Interaction: The dispatched Action is immediately sent to the Store. The Store holds the application's central state.

  • State Update (Reducers): The Store forwards the Action (along with the Previous State) to the Reducers. The Reducers are pure functions that take the old state and the action, process them, and return a new state.

  • View Update: The Store saves this new state. It then notifies all connected components of the View / UI that the Current State has changed. The View automatically re-renders with the fresh data, completing the cycle.

Beyond Prop Drilling: Implementing Redux in Your React Workflow

We've finished our theoretical discussion on Redux. Now, let's explore how to implement Redux within our React project.

There's an excellent tool for this called React Redux-Toolkit.

The Redux-Toolkit makes our work much easier. It's essentially a package that helps us write Redux logic in a highly efficient and standardized way.

No More Boilerplate: The 3 Core Issues Fixed by Redux-Toolkit

  • Configuring a Redux store is too complicated
  • You have to add a lot of packages to get Redux to do anything useful
  • Redux requires too much boilerplate code

Redux Integration: Getting Started with Redux in React

Installation:

yarn: yarn add @reduxjs/toolkit react-redux
npm: npm i @reduxjs/toolkit react-redux

Configure Store:

Create a file named src/app/store.js. Import the configureStore API from Redux Toolkit. We'll start by creating an empty Redux store, and exporting it:

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

export const store = configureStore({
  reducer: {},
});
Enter fullscreen mode Exit fullscreen mode

Provide the Redux Store to React

To make the Redux store available everywhere in our app, we wrap the entire application with the <Provider> component from React-Redux. In the src/index.js file, we simply import the store we created, wrap the main <RouterProvider router={routes}/> (Because I use react-router-dom) component with the , and pass the imported store to it using the store prop.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import { RouterProvider } from "react-router-dom";
import routes from "./router/routes.jsx";
import { Provider } from "react-redux";
import { store } from "./RTK/store/store.js";

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Provider store={store}>
      <RouterProvider router={routes} />
    </Provider>
  </StrictMode>
);
Enter fullscreen mode Exit fullscreen mode

Create Redux State Slice

First, create a new file called src/RTK/features/counter/counterSlice.js. Inside this file, we'll use the createSlice API from Redux Toolkit.

To create a slice, we need three things:

  1. A string name (to identify this part of the state).
  2. An initial state value.
  3. Reducer functions that define how the state can be updated.

Once the slice is created, we can export the action creators and the main reducer function for use throughout our app.

A great feature of Redux Toolkit is that it uses a library called Immer. While Redux normally requires you to write immutable updates (meaning you must copy data before changing it), Immer lets you write simpler, "mutating" logic. Redux Toolkit then automatically translates that simple code into the correct immutable updates behind the scenes.

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

const initialState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

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

export default counterSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

Add Slice Reducers to the Store

The next essential step is integrating this new counterSlice into our main Redux store. We import the reducer function we exported from the slice and add it to the store's reducer parameter. By defining a field for it here (e.g., counter: counterReducer), we are telling Redux: "Use this specific reducer function to manage and handle all updates to the state data under the name 'counter'."

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

export const store = configureStore({
  reducer: {
    counter: counterSlice,
  },
});
Enter fullscreen mode Exit fullscreen mode

Use Redux State and Actions in React Components

Now that the store is set up, we use React-Redux Hooks to let our components talk to it. The useSelector hook allows us to read data directly from the store, and the useDispatch hook gives us the ability to send actions to update that data. To demonstrate, we'll create a new component file, src/RTK/features/counter/Counter.js, put a <Counter> component inside it, and finally, import and render that component within our main file.

const count = useSelector((state) => state.counter.value)
const dispatch = useDispatch()

Example:

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

export function Counter() {
  const count = useSelector((state) => state.counter.value)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Conclusion: Goodbye, Prop Drilling!

You've now successfully implemented the core of Redux in your React application using the Redux Toolkit. By setting up a centralized Store and connecting it with the <Provider>, you have eliminated the messy problem of prop drilling. Now, any component can use useSelector to read state and useDispatch to update state, making your application significantly more predictable and easier to maintain. This scalable structure is key to building large, professional applications.

What's Next? Handling Asynchronous Data

So far, we've only handled local state updates. But what about fetching data from a server or calling an API? In the next article, we'll dive into how Redux handles these crucial asynchronous tasks using a powerful tool within the Toolkit: createAsyncThunk. Stay tuned to learn how to manage complex API calls and loading states the Redux way!

Top comments (0)