DAY 43: Mastered Redux Fundamentals - Store, Actions, Reducers & Data Flow Explained | ReactJS


🚀 Introduction
Welcome to Day 43 of my Web Development Journey!
After building a strong foundation in HTML, CSS, and JavaScript, I’ve been diving deep into ReactJS — a powerful library for building dynamic and interactive user interfaces.
So far, I’ve explored state management in React using useState, Context API, useReducer, and Zustand. Now, I’ve started learning Redux, one of the most popular and robust libraries for state management in modern web applications.
Over the past few days, I’ve learned the basics of Redux, its core terminologies, worked with the combineReducers function, and even implemented a mini Redux by building a custom createStore
and combineReducers
from scratch.
You can check out my complete Redux learning progress in my GitHub repository.
To stay consistent and share my learnings, I’m documenting this journey publicly. Whether you’re just starting with React or looking to strengthen your knowledge of state management, I hope this blog provides something valuable!
Feel free to follow me on Twitter for real-time updates and coding insights.
📅 Here’s What I Covered Over the Last 3 Days
Day 40
- Introduction to Redux
- Why Redux?
- Core Principles
- Redux Terminologies:
- Store
- Action
- Action Creators
- Reducer
- Dispatch
- Subscribe
Day 41
- Built a Mini Redux with Custom
createStore
- Learned about
combineReducers
in Redux
Day 42
- Implemented
combineReducers
from scratch
Let’s dive into these concepts in more detail below 👇
1. Redux Fundamentals:
Here, I’ll explain all the core concepts of Redux:
What is Redux?
Redux is a state management library for JavaScript applications, most commonly used with ReactJS.
It helps manage the global state of an application in a predictable way by keeping it in a single centralized store.
Instead of passing props deeply or managing complex state across multiple components, Redux provides a clear structure for handling data flow.
Why Redux?
In small applications, state can easily be managed using useState or Context API.
However, as applications grow, managing state across multiple components becomes challenging due to:
- Prop drilling (passing props down multiple layers).
- State duplication (same data being managed in different places).
- Complex updates (when multiple components depend on the same piece of state).
Redux solves these problems by:
- Storing all state in a single global store.
- Using actions to describe state changes.
- Using reducers to update state in a predictable, controlled manner.
Core Principles of Redux
Redux is built on three fundamental principles:
Single Source of Truth
- The entire application’s state is stored in one central store.
- This makes debugging and state management easier.
State is Read-Only
- The state cannot be changed directly.
- The only way to modify state is by dispatching an action, which describes what happened.
Changes are Made with Pure Reducers
- Reducers are pure functions that take the current state and an action, then return a new state.
- This ensures predictability and consistency across the application.
Redux Terminologies
Below are the key Redux terminologies:
1. Store
- The store is the central place where the application state lives.
- It holds the state tree and provides methods to access state, dispatch actions, and subscribe to updates.
- The Store has 3 main methods:
getState()
- Read current statedispatch(action)
- Send an action to change statesubscribe(listener)
- Run a function when state changes
import { createStore } from "redux";
// Create store
const store = createStore(reducer);
console.log(store.getState());
2. Action
- An action is a plain JavaScript object that describes what happened in the application
- Every action must have a
type
property, which is a string describing the action. - It can optionally include a
payload
property to pass extra data needed to update the state.
const incrementAction = {
type: "INCREMENT"
};
const addAction = {
type: "ADD",
payload: 5
};
3. Action Creators
- An action creator is a function that returns an action object.
- It helps make the code reusable and consistent.
- Instead of manually creating action objects each time, we call the action creator function.
// Action Creator for increment
const increment = () => {
return {
type: "INCREMENT"
};
};
// Action Creator with payload
const add = (value) => {
return {
type: "ADD",
payload: value
};
};
4. Reducer
- A reducer is a pure function that takes the current state and an action, then returns the new state.
- It decides how the state should change based on the action type.
- It must be pure, meaning it should not modify the original state or produce side effects.
const initialState = { count: 0 };
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
case "ADD":
return { ...state, count: state.count + action.payload };
default:
return state;
}
};
5. Dispatch
- Dispatch is the function used to send an action to the reducer through the store.
- store forwards action -> reducer updates state.
- This is how we trigger state updates.
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "ADD", payload: 5 });
console.log(store.getState()); // { count: 6 }
6. Subscribe
- Subscribe allows us to listen for state changes in the store.
- Whenever the state changes, the subscribed function runs.
store.subscribe(() => {
console.log("State updated:", store.getState());
});
Example
Here’s a complete example demonstrating how to use all these Redux concepts together
import { createStore } from "redux";
// Reducer
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
case "DECREMENT":
return { count: state.count - 1 };
case "ADD":
return { count: state.count + action.payload };
default:
return state;
}
}
// Store
const store = createStore(counterReducer);
// Action Creators
function increment() {
return { type: "INCREMENT" };
}
function decrement() {
return { type: "DECREMENT" };
}
function add(value) {
return { type: "ADD", payload: value };
}
// Subscribe to changes
store.subscribe(() => {
console.log("Updated State:", store.getState());
});
// Dispatch actions
store.dispatch(increment()); // count = 1
store.dispatch(add(5)); // count = 6
store.dispatch(decrement()); // count = 5
Explanation
- Store holds the state, e.g.,
{ count: 0 }
. - Dispatch sends actions like
{ type: "INCREMENT" }
. - Reducer receives the action and updates the state accordingly.
- Subscribe logs the updated state every time it changes.
This small example demonstrates the Redux data flow:Dispatch → Reducer → Store → Subscribe
2. Build a Mini Redux with Custom createStore:
To better understand how Redux works internally, I implemented my own version of the createStore
function.
This function will return a store object that provides three main methods:
getState()
→ To read the current statedispatch(action)
→ To update state by sending actions to the reducersubscribe(listener)
→ To listen for state changes
Implementation
export function createStore(reducer) {
let state;
const listeners = [];
const store = {
// Get current state
getState: () => {
return state;
},
// Dispatch an action -> update state
dispatch: (action) => {
state = reducer(state, action);
listeners.forEach((listener) => {
listener(); // notify all subscribers
});
},
// Subscribe to state changes
subscribe: (listener) => {
listeners.push(listener);
// Return unsubscribe function
return function () {
const listenerIndex = listeners.findIndex(
(registeredListener) => registeredListener === listener
);
listeners.splice(listenerIndex, 1);
};
},
};
// Initialize state by dispatching a dummy action
store.dispatch({ type: "@@INIT" });
return store;
}
Explanation
1. State & Listeners
state
holds the current application state.listeners
is an array of functions that should be called whenever the state changes.
2. getState()
- Returns the current state value.
- Similar to
store.getState()
in Redux.
3. dispatch(action)
- Sends an action to the reducer.
- The reducer calculates the new state and updates
state
. - After updating, it runs all subscribed listener functions.
4. subscribe(listener)
- Registers a new listener (callback function) that should be executed whenever the state updates.
- Returns an unsubscribe function that removes the listener when called.
- This ensures we don’t keep unnecessary listeners around.
5. Initialization
- At the end,
store.dispatch({ type: "@@INIT" })
is called to initialize the state with the reducer’s default value.
With this simple createStore
implementation, I’ve essentially recreated the core behavior of Redux’s store:
A predictable state container with getState, dispatch, and subscribe.
3. combineReducers in Redux:
combineReducers is a utility function provided by Redux that allows us to split the state management logic into multiple reducers.
Instead of having one large reducer that handles everything, we can break it down into smaller reducers, each responsible for a specific part of the state.
Redux will then combine these smaller reducers into a single root reducer using combineReducers
.
What does combineReducers
do?
- It takes an object of reducers and returns a single reducer function.
- Each key in the object corresponds to a slice of the state.
- When an action is dispatched, Redux will call all reducers, but only the reducer responsible for the relevant slice will update its part of the state.
- Finally combineReducers combines all slice states into a single global state object.
State Shape with combineReducers
When using combineReducers
, the state tree will match the structure of the reducer object.
For example:
import { combineReducers } from "redux";
// Reducer for counter
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case "INCREMENT":
return { count: state.count + 1 };
default:
return state;
}
}
// Reducer for todos
function todoReducer(state = [], action) {
switch (action.type) {
case "ADD_TODO":
return [...state, action.payload];
default:
return state;
}
}
// Root reducer
const rootReducer = combineReducers({
counter: counterReducer,
todos: todoReducer,
});
The resulting state shape will look like this:
{
counter: { count: 0 },
todos: []
}
So, state is structured automatically based on the keys we provide
Benefits of combineReducers
- Separation of Concerns → Each reducer manages only its own slice of the state.
- Code Organization → Makes large Redux projects more manageable and scalable.
- Reusability → Individual reducers can be reused in different projects or features.
- Debugging Made Easy → Since each reducer handles only a small part of state, debugging is simpler.
4. Implementing combineReducers from Scratch:
To understand how Redux works internally, I implemented a simple version of the combineReducers
function.
This custom function allows us to merge multiple reducers into one main reducer, similar to Redux’s built-in implementation.
Code Implementation
// Custom combineReducers Function
function combineReducers(reducers) {
return function (state = {}, action) {
const newState = {};
for (let key in reducers) {
const reducer = reducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
newState[key] = nextStateForKey;
}
return newState;
};
}
Explanation
1. Input (reducers object)
- The function accepts an object where each key represents a slice of the state, and the value is the corresponding reducer function.
2. Returned Function (Main Reducer)
- It returns a new reducer function that manages the overall state object.
3. Iterating Through Reducers
- For each key in the reducers object:
- Get the reducer function for that slice.
- Pass the corresponding slice of state (
previousStateForKey
) and the currentaction
. - Store the updated state in
newState[key]
.
4. Final Output
- After processing all reducers,
newState
(the updated global state object) is returned.
By breaking down the state into slices and delegating responsibility to different reducers, this custom combineReducers
function ensures modularity and separation of concerns, just like Redux’s official implementation.
5. What’s Next:
I’m excited to keep growing and sharing along the way! Here’s what’s coming up:
- Posting new blog updates every 3 days to share what I’m learning and building.
- Diving deeper into Data Structures & Algorithms with Java — check out my ongoing DSA Journey Blog for detailed walkthroughs and solutions.
- Sharing regular progress and insights on X (Twitter) — feel free to follow me there and join the conversation!
Thanks for being part of this journey!
6. Conclusion:
Over the last three days, I’ve taken a deep dive into Redux fundamentals, built a mini Redux implementation from scratch, and explored the power of combineReducers to manage complex state in a modular way.
Here’s the key takeaway:
Redux may feel a bit boilerplate-heavy at first, but its predictable data flow, strict structure, and debugging advantages make it one of the most reliable state management solutions for large-scale applications.
By implementing createStore
and combineReducers
manually, I not only understood how Redux works internally but also developed a deeper appreciation for why it’s designed this way.
As I continue this journey, I’ll be moving towards:
- Connecting Redux with React using react-redux
- Understanding middlewares like redux-thunk
- Exploring advanced concepts such as Redux Toolkit (RTK)
If you’re following along, my advice is:
Don’t just learn Redux APIs — try implementing them yourself. It’s one of the best ways to truly understand the “why” behind the “how.”
Thanks for reading! Feel free to connect or follow along as I continue building and learning in React.
Subscribe to my newsletter
Read articles from Ritik Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Ritik Kumar
Ritik Kumar
👨💻 Aspiring Software Developer | MERN Stack Developer.🚀 Documenting my journey in Full-Stack Development & DSA with Java.📘 Focused on writing clean code, building real-world projects, and continuous learning.