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?
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
, andCmp-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 theSignin
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.
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
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
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 usingdispatch(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 arepure functions
that take theold state
and theaction
, 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: {},
});
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>
);
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:
- A string name (to identify this part of the state).
- An initial state value.
- 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;
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,
},
});
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>
)
}
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)