Welcome back to Episode 12 of the “Let’s Master React Hooks Together” series
So far in this series, we’ve explored React hooks, component architecture, side effects, refs, memoization, and performance optimization. As React applications grow larger and more feature-rich, one major challenge starts appearing everywhere — managing state across multiple components efficiently.
What begins as simple prop passing between parent and child components can eventually turn into deeply nested component trees, duplicated state, inconsistent updates, and difficult-to-maintain code.
This problem becomes especially noticeable in real-world applications like:
- E-commerce platforms
- Social media applications
- Admin dashboards
- Analytics platforms
- Project management tools
In these applications, multiple components often need access to the same shared data at the same time.
That’s exactly where Redux becomes valuable.
Redux helps React applications manage state in a centralized, predictable, and scalable way. Instead of scattering shared state across many components, Redux introduces a single source of truth that makes data flow easier to understand, debug, and maintain.
In this article, we’ll deeply understand Redux, how it works with React, why Redux Toolkit is now the standard approach, and how to structure Redux applications properly for modern frontend development.
By the end of this article, you’ll clearly understand:
- What Redux is
- Why Redux exists
- Core Redux concepts
- Redux Toolkit
- Store setup
- Actions and reducers
- Async operations
- RTK Query
- Best practices
- Common mistakes
What is Redux?
Redux is a predictable state management library used for JavaScript applications, especially React applications.
It provides a centralized place called a store where the entire application state is managed in a structured and organized way.
Instead of multiple components independently managing and duplicating shared data, Redux creates a single source of truth for the application.
This centralized architecture makes state changes easier to track, debug, test, and maintain as applications become larger and more complex.
Redux follows a strict one-way data flow, which makes application behavior more predictable and easier to understand during development.
Why Redux is Important
In modern frontend applications, multiple components often depend on the same piece of data.
For example:
- Navbar → cart count
- Product page → add to cart
- Checkout page → cart items
- Wishlist page → saved products
- Profile page → user details
- Dashboard → analytics data
Managing this shared data separately inside individual components quickly becomes difficult and error-prone.
Without centralized state management:
- State duplication increases
- Data synchronization becomes inconsistent
- Prop drilling becomes excessive
- Debugging becomes harder
- Component communication becomes messy
- Scaling the application becomes difficult
Redux solves these issues by introducing a centralized and predictable state container that every component can access consistently.
The Problem Redux Solves
Consider this component structure:
<App>
<Dashboard>
<Sidebar>
<Profile />
</Sidebar>
</Dashboard>
</App>
If Profile needs user data stored inside App, the data must pass through every intermediate component.
This process is called prop drilling.
As applications grow, prop drilling creates several problems:
- Components receive props they don’t actually use
- Code becomes harder to maintain
- Component trees become cluttered
- State management becomes difficult
Redux removes this problem completely by allowing components to directly access centralized state from anywhere inside the application.
Redux Mental Model
You can think of Redux like a banking system:
- Store → Bank database
- Action → Request form
- Reducer → Bank employee processing request
- Dispatch → Submitting the request
- Selector → Reading account information
This analogy helps visualize how data flows inside Redux applications.
Core Redux Concepts
To understand how Redux works internally, we first need to understand the five core building blocks that power the Redux architecture.
Redux mainly revolves around five concepts:
- Store
- Actions
- Reducers
- Dispatch
- Selectors
Let’s understand them one by one.
1. Store
The store is the central storage for the application state.
It acts as the brain of the application and contains the complete global state tree for the entire application.
Example:
{
user: {
name: "John"
},
cart: [],
theme: "dark"
}
In this example:
-
userstores authentication or profile-related data -
cartstores shopping cart items -
themestores UI preferences
The entire application state lives inside one centralized object, making shared data easier to manage consistently across the application.
2. Actions
Actions describe what happened inside the application.
They are plain JavaScript objects that tell Redux what type of state update should occur.
Example:
{
type: "cart/addItem",
payload: {
id: 1,
name: "Laptop"
}
}
Here:
-
typeidentifies the action -
"cart/addItem"describes adding an item to cart -
payloadcarries extra data -
idrepresents product ID -
namerepresents product name
Actions help maintain predictable state updates because every state change is explicitly described.
3. Reducers
Reducers are pure functions responsible for determining how the state should change in response to specific actions.
They receive the current state and an action object, then return a brand-new updated state.
Reducers never perform API calls, side effects, or asynchronous operations directly because their behavior must remain predictable and consistent.
Example:
function cartReducer(state = [], action) {
switch (action.type) {
case "cart/addItem":
return [...state, action.payload];
default:
return state;
}
}
Here:
-
cartReduceris the reducer function -
state = []sets default cart state -
actionreceives dispatched action object -
switch(action.type)checks action type -
"cart/addItem"handles cart item addition -
[...state, action.payload]creates new updated array -
defaultreturns existing state if no match exists
Reducers are one of the most important parts of Redux because they define how state transitions happen in a predictable way.
4. Dispatch
Dispatch acts as the communication bridge between components and the Redux store.
Whenever a user performs an action like clicking a button, submitting a form, or updating data, components dispatch actions to Redux.
Redux then forwards those actions to reducers, which calculate the next state update.
Example:
dispatch({
type: "cart/addItem",
payload: product
});
Here:
-
dispatch()sends action to Redux store -
typeidentifies the operation -
payloadcontains product data - Reducer receives this action
- Redux updates state automatically
5. Selectors
Selectors are used to read data from the Redux store.
They help components subscribe only to the specific pieces of state they need.
This improves performance because components re-render only when the selected state changes.
Example:
const cartItems = useSelector(
(state) => state.cart
);
Here:
-
useSelectoraccesses Redux state -
staterepresents complete Redux store -
state.cartextracts cart data -
cartItemsstores extracted values
Selectors help keep components clean and focused by separating state access logic from UI rendering logic.
Redux Data Flow
Redux follows a strict one-way data flow architecture.
The flow looks like this:
- User interacts with UI
- Action gets dispatched
- Reducer processes action
- Store updates state
- UI re-renders automatically
This predictable structure makes applications easier to debug, scale, and maintain because every state update follows the same consistent flow.
What is Redux Toolkit?
Redux Toolkit is the official recommended way to use Redux today.
Earlier Redux implementations required developers to manually create:
- Action types
- Action creators
- Reducers
- Immutable update logic
- Middleware configuration
- Redux DevTools setup
This resulted in large amounts of repetitive boilerplate code.
Redux Toolkit was introduced to solve these problems and provide a modern, standardized, and simplified Redux development experience.
Benefits of Redux Toolkit:
- Less code
- Cleaner structure
- Built-in best practices
- Better readability
- Simpler async handling
- Automatic DevTools setup
- Easier state updates
Redux Toolkit dramatically improves developer productivity while still following Redux principles internally.
Installing Redux Toolkit
npm install @reduxjs/toolkit react-redux
-
@reduxjs/toolkitinstalls Redux Toolkit -
react-reduxconnects Redux with React - Both packages are required for React Redux applications
Creating a Redux Slice
A slice is one of the most important concepts in Redux Toolkit.
A slice groups together:
- State
- Reducers
- Actions
This feature-based structure keeps Redux code cleaner and easier to scale.
Create counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
value: 0
};
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;
-
createSlicecreates Redux slice -
initialStatestores default values -
value: 0initializes counter -
name: "counter"defines slice name -
reducerscontains state update functions -
incrementincreases counter value -
decrementdecreases counter value -
incrementByAmountupdates value dynamically -
action.payloadreceives custom value -
counterSlice.actionsauto-generates actions -
export default counterSlice.reducerexports reducer
Slices significantly reduce Redux boilerplate and organize related logic in one place.
Why Direct Mutation Works Here
Inside Redux Toolkit:
state.value += 1;
At first glance, this looks like direct state mutation.
In traditional Redux, directly mutating state is strictly forbidden because Redux depends on immutable updates to detect state changes correctly.
However, Redux Toolkit internally uses a library called Immer.
Immer allows developers to write code that appears mutable while safely generating immutable updates behind the scenes.
So even though the syntax looks like mutation:
state.value += 1;
Redux Toolkit still preserves immutability internally.
This makes Redux code much cleaner and easier to write.
Configuring Redux Store
The Redux store holds the complete application state.
Create store.js
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer
}
});
-
configureStorecreates Redux store -
counterReducerimports reducer -
reducerobject combines reducers -
counterbecomes state key - Entire application now accesses centralized state
Redux Toolkit’s configureStore also automatically enables useful development features like Redux DevTools and middleware support.
Connecting Redux to React
To make Redux available throughout the React application, React Redux provides a component called Provider.
Inside main.jsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { Provider } from "react-redux";
import { store } from "./store";
ReactDOM.createRoot(
document.getElementById("root")
).render(
<Provider store={store}>
<App />
</Provider>
);
-
Providerconnects React with Redux -
store={store}passes Redux store globally -
<App />gets access to Redux state - Every child component can now use Redux hooks
Without Provider, React components cannot access the Redux store.
Using Redux Inside Components
React Redux provides hooks that allow components to interact with Redux easily.
import React from "react";
import {
useSelector,
useDispatch
} from "react-redux";
import {
increment,
decrement
} from "./counterSlice";
function Counter() {
const count = useSelector(
(state) => state.counter.value
);
const dispatch = useDispatch();
return (
<div>
<h1>{count}</h1>
<button
onClick={() => dispatch(increment())}
>
Increment
</button>
<button
onClick={() => dispatch(decrement())}
>
Decrement
</button>
</div>
);
}
export default Counter;
-
useSelectorreads Redux state -
state.counter.valueaccesses counter value -
countstores extracted state -
useDispatchreturns dispatch function -
dispatch(increment())triggers increment action -
dispatch(decrement())triggers decrement action - UI automatically updates after state changes
This hook-based approach makes Redux integration significantly cleaner in modern React applications.
Recommended Redux Folder Structure
As Redux applications grow, organizing files properly becomes very important.
A feature-based structure improves scalability and maintainability.
src/
├── app/
│ └── store.js
├── features/
│ ├── counter/
│ │ ├── counterSlice.js
│ │ └── Counter.jsx
├── components/
-
app/contains global store setup -
features/stores feature-based Redux logic -
counter/contains counter-related files -
components/stores reusable UI components
This structure keeps Redux logic modular and easier to scale in large applications.
Async Operations with createAsyncThunk
Real-world applications rarely work with static data.
Most applications fetch information from APIs such as:
- User data
- Product lists
- Orders
- Notifications
- Dashboard analytics
Handling asynchronous operations manually in Redux can become complicated.
Redux Toolkit simplifies async logic using createAsyncThunk.
import {
createSlice,
createAsyncThunk
} from "@reduxjs/toolkit";
export const fetchUsers =
createAsyncThunk(
"users/fetchUsers",
async () => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
return response.json();
}
);
-
createAsyncThunkhandles async logic -
"users/fetchUsers"defines action name -
async()performs API request -
fetch()calls backend API -
response.json()converts response into JSON - Returned data becomes payload automatically
createAsyncThunk also automatically generates loading, success, and error action types internally.
Redux DevTools
Redux DevTools is one of Redux’s most powerful debugging features.
It helps developers:
- Track dispatched actions
- Inspect state updates
- Debug application flow
- Replay state changes
- Analyze application behavior
Redux Toolkit automatically enables Redux DevTools during development, making debugging significantly easier.
Redux vs Context API
React Context API can also handle shared state.
However, Redux provides several advanced capabilities that become valuable in larger applications.
Redux offers:
- Better debugging tools
- Middleware support
- Predictable architecture
- Advanced async handling
- Improved scalability
- Better developer tooling
Context API works well for:
- Theme switching
- Authentication state
- Language preferences
- Small shared states
Redux becomes more powerful when applications grow larger and state management complexity increases.
Common Redux Mistakes
Direct State Mutation
Wrong:
state.items.push(item);
Chain-by-chain breakdown:
- Direct array mutation occurs
- Original state gets modified
- Traditional Redux does not allow this
- Mutations can break predictable state updates
Correct:
return [...state, item];
- Creates new array
- Keeps original state immutable
- Redux state updates safely
Understanding immutability is one of the most important concepts in Redux architecture.
RTK Query
RTK Query is one of the most powerful features introduced in Redux Toolkit.
It eliminates much of the manual API handling logic developers traditionally wrote themselves.
RTK Query automatically manages:
- Data fetching
- Caching
- Loading states
- Error handling
- Background refetching
- Request deduplication
This significantly reduces boilerplate and improves application performance.
Example:
import {
createApi,
fetchBaseQuery
} from "@reduxjs/toolkit/query/react";
export const api = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "https://api.example.com"
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => "/users"
})
})
});
export const {
useGetUsersQuery
} = api;
-
createApicreates API service -
reducerPathdefines Redux key -
fetchBaseQuerysets base API URL -
endpointsdefines API operations -
builder.query()creates GET request -
query()returns endpoint path -
useGetUsersQuerybecomes auto-generated hook
RTK Query dramatically simplifies server-state management in modern React applications.
Best Practices
Use Redux Toolkit
Redux Toolkit is now the official standard for Redux development because it reduces boilerplate, improves readability, and includes built-in best practices.
Keep State Minimal
Only store data globally if multiple components truly need access to it.
Avoid storing unnecessary UI state inside Redux.
Use Feature-Based Structure
Keep related Redux logic together inside feature folders.
This improves scalability and maintainability.
Avoid Overusing Redux
Not every piece of state belongs in Redux.
Local UI states like:
- Modal visibility
- Form inputs
- Dropdown toggles
- Tooltip visibility
are usually better managed using useState.
Final Thoughts
This was Episode 12 of the “Let’s Master React Hooks Together” series, and Redux is one of the most important concepts to understand once React applications begin scaling.
Redux may initially feel complex because it introduces several new architectural concepts. However, once you understand the predictable flow between actions, reducers, and the store, managing large-scale React applications becomes significantly more organized and maintainable.
Modern Redux with Redux Toolkit has dramatically simplified the developer experience, making Redux cleaner, easier to learn, and far more practical for real-world applications.
The goal is not just learning Redux syntax, but understanding how scalable frontend architecture works in professional applications.
Once you become comfortable with slices, reducers, actions, selectors, and async handling, building complex applications becomes significantly easier and more structured.
In the upcoming episodes, we’ll continue exploring advanced React concepts, scalable frontend architecture, performance optimization patterns, and production-level development practices used in modern applications.
See you in the next episode of “Let’s Master React Hooks Together.”
Top comments (0)