Master Redux with Interactive Visual Notes, Interview Q&A, and Comprehensive Definitions
Imagine you have a big toy box (Store) where all your toys are kept. When you want to play with a toy, you tell your mom (Dispatch an Action). Your mom follows the rules (Reducer) to get the toy from the box and give it to you. Everyone in the family can see which toys are in the box (State), but only mom can put toys in or take them out following the rules!
Redux is a predictable state container for JavaScript apps. It helps you manage the "global" state - data that many parts of your app need to access and update. Think of it as a central database for your app's data!
The Store is like a big treasure chest that holds ALL the data for your entire app. There's only ONE treasure chest, and everyone can look inside it, but only special people (reducers) can change what's inside!
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
// Add more reducers here
},
});
export default store;
An Action is like a message or instruction. It's like saying "Hey, I want to add 1 to the counter!" or "I want to delete this item!". It describes WHAT happened, not HOW to do it.
// Action is just a plain JavaScript object
const action = {
type: 'counter/increment', // What happened
payload: 5 // Optional data
};
// With Redux Toolkit, actions are created automatically!
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 },
reducers: {
increment(state) {
state.value += 1;
},
incrementByAmount(state, action) {
state.value += action.payload;
}
}
});
// Actions are automatically created!
export const { increment, incrementByAmount } = counterSlice.actions;
A Reducer is like a rule book. It says "IF this action happens, THEN do this to the state". It's a function that takes the old state and an action, and returns the NEW state. It's like a recipe - you follow the steps to make something new!
import { createSlice } from '@reduxjs/toolkit';
const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
// Redux Toolkit allows "mutating" logic!
// It uses Immer library internally
addTodo(state, action) {
state.push({
id: Date.now(),
text: action.payload,
completed: false
});
},
toggleTodo(state, action) {
const todo = state.find(t => t.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo(state, action) {
return state.filter(t => t.id !== action.payload);
}
}
});
export const { addTodo, toggleTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
Dispatch is like pressing a button or sending a letter. When you want something to happen, you "dispatch" an action. It's the ONLY way to change the state in Redux!
import { useDispatch } from 'react-redux';
import { increment, incrementByAmount } from './counterSlice';
function Counter() {
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(increment())}
Add 1
</button>
<button onClick={() => dispatch(incrementByAmount(5))}
Add 5
</button>
</div>
);
}
A Selector is like a magnifying glass that helps you look at specific parts of the treasure chest (store). Instead of looking at everything, you can use a selector to find exactly what you need!
import { useSelector } from 'react-redux';
function Counter() {
// Select specific data from the store
const count = useSelector(state => state.counter.value);
const todos = useSelector(state => state.todos);
// You can also compute derived data
const completedTodos = useSelector(state =>
state.todos.filter(t => t.completed)
);
return (
<div>
<p>Count: {count}</p>
<p>Completed: {completedTodos.length}</p>
</div>
);
}
Middleware is like a security guard or helper that stands between you sending a message (action) and it reaching the rule book (reducer). It can check the message, change it, or even stop it! It's useful for things like logging, making API calls, or handling errors.
const loggerMiddleware = store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('Next state:', store.getState());
return result;
};
// Redux Toolkit includes Redux Thunk middleware by default!
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
});
Redux Toolkit is the official, recommended way to write Redux logic. It simplifies Redux code and includes best practices by default!
configureStore instead of createStorecreateSlice to create reducers and actions togethercreateAsyncThunk for async logiccreateSelector for memoized selectorsimport { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// Async thunk for API calls
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
loading: 'idle',
error: null
},
reducers: {
userAdded(state, action) {
state.entities.push(action.payload);
},
userUpdated(state, action) {
const { id, changes } = action.payload;
const user = state.entities.find(u => u.id === id);
if (user) {
Object.assign(user, changes);
}
}
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = 'pending';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = 'idle';
state.entities = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = 'idle';
state.error = action.error.message;
});
}
});
export const { userAdded, userUpdated } = usersSlice.actions;
export default usersSlice.reducer;
Directly mutating state breaks Redux's immutability principle and causes bugs!
// ❌ WRONG - Direct mutation (without Redux Toolkit)
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
state.push(action.payload); // ❌ Mutates state!
return state;
}
}
// ✅ CORRECT - Immutable update (without Redux Toolkit)
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload]; // ✅ New array!
}
}
// ✅ BEST - Redux Toolkit (Immer handles immutability)
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo(state, action) {
state.push(action.payload); // ✅ Looks like mutation, but Immer makes it immutable!
}
}
});
Redux state should only contain plain JavaScript objects, arrays, and primitives. No functions, Promises, Dates, or class instances!
// ❌ WRONG - Non-serializable values
const badState = {
user: {
name: 'John',
createdAt: new Date(), // ❌ Date object
fetchData: () => {}, // ❌ Function
promise: fetch('/api') // ❌ Promise
}
};
// ✅ CORRECT - Serializable values
const goodState = {
user: {
name: 'John',
createdAt: '2024-01-01T00:00:00Z', // ✅ ISO string
isLoading: false, // ✅ Boolean
data: null // ✅ Null
}
};
Multiple async operations can complete in unexpected order, causing stale data!
import { createAsyncThunk } from '@reduxjs/toolkit';
// ✅ Solution: Use createAsyncThunk with condition
export const fetchUser = createAsyncThunk(
'users/fetchUser',
async (userId, { getState, requestId }) => {
const { currentRequestId, loading } = getState().users;
// Cancel if there's already a request in progress
if (loading !== 'pending' || requestId !== currentRequestId) {
return;
}
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
{
condition: (userId, { getState }) => {
const { loading } = getState().users;
// Don't fetch if already loading
return loading !== 'pending';
}
}
);
Instead of deeply nested objects, use a flat structure with IDs as keys!
// ❌ BAD - Nested structure
const badState = {
posts: [
{
id: 1,
title: 'Post 1',
author: {
id: 1,
name: 'John',
posts: [/* circular reference! */]
},
comments: [
{ id: 1, text: 'Comment 1', author: {/* ... */} }
]
}
]
};
// ✅ GOOD - Normalized structure
const goodState = {
posts: {
byId: {
1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] }
},
allIds: [1]
},
users: {
byId: {
1: { id: 1, name: 'John', postIds: [1] }
},
allIds: [1]
},
comments: {
byId: {
1: { id: 1, text: 'Comment 1', authorId: 1 }
},
allIds: [1]
}
};
// Use createEntityAdapter for automatic normalization!
import { createEntityAdapter } from '@reduxjs/toolkit';
const postsAdapter = createEntityAdapter();
const initialState = postsAdapter.getInitialState();
Answer:
Redux ek predictable state container hai JavaScript apps ke liye. Ye ek central store hai jahan app ka saara data ek jagah store hota hai.
Kab use karein:
Kab use NAHI karein:
Real example: E-commerce app mein user info, cart items, product list - ye sab different components mein chahiye. Redux se ye sab ek jagah manage ho jata hai!
Answer:
Redux ka data flow ek one-way street hai:
1. Action (📤):
type (mandatory) aur payload (optional data) hota hai
{ type: 'ADD_TODO', payload: 'Buy milk' }2. Dispatch (🚀):
dispatch(addTodo('Buy milk'))3. Reducer (⚙️):
(previousState, action)newState4. Store (🗄️):
Flow: User clicks button → Dispatch action → Reducer processes → Store updates → Components re-render
Answer:
Redux Toolkit (RTK) Redux ka official, recommended way hai. Ye Redux ko simple aur powerful banata hai!
Purane Redux ki problems:
Redux Toolkit ke benefits:
Example comparison:
Purana Redux: 50 lines of code
Redux Toolkit: 15 lines of code (same functionality!)
Interview tip: Hamesha bolo ki aap Redux Toolkit use karte ho, purana way outdated hai!
Answer:
Pure function ka matlab:
Reducer pure kyu ho:
Side effects examples (❌ Reducer mein NAHI karna):
Side effects kahan karein (✅):
Interview tip: Agar interviewer puchhe "API call kahan karoge?" - kabhi mat bolo "reducer mein"! Bolo "createAsyncThunk ya middleware mein"
Answer:
Direct mutation ki problem:
Purane Redux mein solution:
{ ...state, newProp: value }[...state, newItem]Redux Toolkit ka magic (Immer):
state.value += 1Example:
// Purana Redux - Complex
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, completed: !todo.completed }
: todo
)
};
// Redux Toolkit - Simple!
const todo = state.todos.find(t => t.id === action.id);
todo.completed = !todo.completed;
Interview tip: Bolo "Redux Toolkit mein Immer built-in hai, isliye mutating syntax use kar sakte hain safely!"
Answer:
useSelector (🎯):
useDispatch (🚀):
Performance tip:
Example:
import { useSelector, useDispatch } from 'react-redux';
function TodoList() {
// Read data from store
const todos = useSelector(state => state.todos);
const count = useSelector(state => state.todos.length);
// Get dispatch function
const dispatch = useDispatch();
// Dispatch actions
const handleAdd = () => {
dispatch(addTodo('New todo'));
};
}
Interview tip: Agar performance question aaye, bolo "createSelector use karta hun memoization ke liye"
Answer:
Reducers pure functions hain, isliye async operations directly nahi kar sakte. Redux Toolkit mein createAsyncThunk use karte hain!
createAsyncThunk kya karta hai:
3 States:
Complete example:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
// Create async thunk
export const fetchUsers = createAsyncThunk(
'users/fetchUsers',
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
const usersSlice = createSlice({
name: 'users',
initialState: {
data: [],
loading: false,
error: null
},
extraReducers: (builder) => {
builder
.addCase(fetchUsers.pending, (state) => {
state.loading = true;
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.data = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
Alternative: Redux Thunk (purana way) ya Redux Saga (complex apps ke liye)
Interview tip: Bolo "createAsyncThunk Redux Toolkit ka recommended way hai async operations ke liye"
Answer:
Middleware ek interceptor hai jo action dispatch hone aur reducer tak pahunchne ke beech mein kaam karta hai.
Middleware ka use:
Popular middleware:
Custom middleware example:
// Logger middleware
const logger = store => next => action => {
console.log('Dispatching:', action);
const result = next(action);
console.log('Next state:', store.getState());
return result;
};
// Add to store
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger)
});
Flow: Dispatch → Middleware 1 → Middleware 2 → Reducer → Store
Interview tip: Bolo "Redux Toolkit mein Redux Thunk by default included hai, isliye async operations easily handle ho jate hain"
Answer:
Redux DevTools ek browser extension hai jo Redux debugging ko super easy banata hai!
Features:
Setup (Redux Toolkit mein automatic!):
import { configureStore } from '@reduxjs/toolkit';
// Redux Toolkit automatically enables DevTools!
const store = configureStore({
reducer: rootReducer,
// DevTools enabled by default in development
});
Kaise use karein:
Pro tips:
Interview tip: Bolo "Redux DevTools development mein bahut helpful hai, time-travel debugging se bugs quickly fix ho jate hain"
Answer:
Ye bahut common interview question hai! Dono ka apna use case hai.
Context API use karo jab:
Redux use karo jab:
Performance difference:
Comparison table:
| Feature | Context API | Redux |
|---|---|---|
| Setup | Simple | More setup needed |
| Performance | Can cause unnecessary re-renders | Optimized with selectors |
| DevTools | No | Yes (Redux DevTools) |
| Middleware | No | Yes |
| Learning curve | Easy | Steeper |
Best practice: Dono ko saath mein bhi use kar sakte ho! Context API for simple things, Redux for complex state.
Interview tip: Bolo "Context API aur Redux competitors nahi hain, dono ka apna use case hai. Small apps mein Context API sufficient hai, large apps mein Redux better hai"
Redux is a predictable state container for JavaScript applications. It helps you write applications that behave consistently, run in different environments (client, server, and native), and are easy to test. Redux centralizes your application's state and logic, enabling powerful capabilities like undo/redo, state persistence, and time-travel debugging.
Redux is like a big toy box for your app's data. Instead of every room (component) having its own toys (data), there's ONE central toy box where all toys are kept. Everyone can see what's in the box, but there are rules about how to add or remove toys. This makes it easy to know where everything is and what's happening!
E-commerce Application: In an online shopping app, you need to manage user authentication, shopping cart items, product listings, order history, and user preferences. Without Redux, passing this data between components becomes messy (props drilling). With Redux, all this data lives in one central store, and any component can access what it needs!
The Store is the central repository that holds the entire state tree of your
application. There is only one store in a Redux application. The store has the following
responsibilities: holds application state, allows access to state via getState(),
allows state to be updated via dispatch(action), registers listeners via
subscribe(listener), and handles unregistering of listeners.
The Store is like a bank vault. It safely keeps all your app's important information (money/data). You can check what's inside (getState), but you can't just walk in and take things. You have to follow the bank's rules (dispatch actions) to make changes. The bank also tells you when something changes (subscribe)!
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';
import userReducer from './features/user/userSlice';
const store = configureStore({
reducer: {
counter: counterReducer,
user: userReducer,
},
// Redux DevTools enabled by default
// Middleware (Redux Thunk) included by default
});
export default store;
An Action is a plain JavaScript object that describes an intention to change the
state. Actions are the only way to get data into the store. Every action must have a
type property (string) that indicates the type of action being performed. Actions
can optionally have a payload property containing additional data needed to perform
the action.
An Action is like a note or message you write. It says "Hey, I want to do something!" For example, "I want to add a toy to the box" or "I want to remove this toy". The note describes WHAT you want to do, not HOW to do it. Someone else (the reducer) will read the note and do the actual work!
// Simple action (no payload)
{
type: 'counter/increment'
}
// Action with payload
{
type: 'counter/incrementByAmount',
payload: 5
}
// Complex action
{
type: 'todos/todoAdded',
payload: {
id: 1,
text: 'Learn Redux',
completed: false
}
}
A Reducer is a pure function that takes the previous state and an action, and
returns the next state: (previousState, action) => newState. Reducers specify how
the application's state changes in response to actions. They must be pure functions - same
inputs always produce the same output, with no side effects. Reducers should never mutate the
existing state; instead, they should return a new state object.
A Reducer is like a recipe book with rules. When someone gives you a note (action) saying "add a toy", you look in the recipe book to see HOW to add the toy. The recipe says "take the old toy box, make a NEW toy box with all the old toys PLUS the new toy". You never change the old toy box directly - you always make a new one!
import { createSlice } from '@reduxjs/toolkit';
const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
history: []
},
reducers: {
// Redux Toolkit uses Immer, so we can "mutate" state
increment(state) {
state.value += 1;
state.history.push('increment');
},
decrement(state) {
state.value -= 1;
state.history.push('decrement');
},
incrementByAmount(state, action) {
state.value += action.payload;
state.history.push(`increment by ${action.payload}`);
},
reset(state) {
state.value = 0;
state.history = [];
}
}
});
export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions;
export default counterSlice.reducer;
Dispatch is a function provided by the Redux store that is used to send
(dispatch) actions to the store. It is the only way to trigger a state change. When you call
dispatch(action), the store runs its reducer function with the current state and
the dispatched action, and saves the returned new state. It then notifies all subscribers that
the state has been updated.
Dispatch is like pressing a doorbell or sending a letter. When you want something to happen, you "dispatch" (send) a message. It's like telling the mailman "Please deliver this letter to the toy box manager!" The dispatch function makes sure your message (action) gets to the right place (reducer).
import { useDispatch, useSelector } from 'react-redux';
import { increment, decrement, incrementByAmount } from './counterSlice';
function Counter() {
const count = useSelector(state => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}
+1
</button>
<button onClick={() => dispatch(decrement())}
-1
</button>
<button onClick={() => dispatch(incrementByAmount(10))}
+10
</button>
</div>
);
}
A Selector is a function that extracts specific pieces of data from the Redux store state. Selectors can compute derived data, allowing Redux to store the minimal possible state. They are efficient because they can be memoized using libraries like Reselect, preventing unnecessary recalculations. Selectors help keep your components decoupled from the exact shape of the state tree.
A Selector is like a magnifying glass or a search tool. Instead of looking at the entire toy box (which might be huge!), you use the selector to find exactly what you need. "Show me only the red toys" or "Count how many toys I have". It helps you find specific things quickly without looking through everything!
import { useSelector } from 'react-redux';
import { createSelector } from '@reduxjs/toolkit';
// Simple selector
const selectTodos = state => state.todos;
// Memoized selector (better performance)
const selectCompletedTodos = createSelector(
[selectTodos],
(todos) => todos.filter(todo => todo.completed)
);
const selectTodoStats = createSelector(
[selectTodos],
(todos) => ({
total: todos.length,
completed: todos.filter(t => t.completed).length,
active: todos.filter(t => !t.completed).length
})
);
// Using in component
function TodoList() {
const todos = useSelector(selectTodos);
const completedTodos = useSelector(selectCompletedTodos);
const stats = useSelector(selectTodoStats);
// ...
}
Middleware provides a third-party extension point between dispatching an action
and the moment it reaches the reducer. Middleware can be used for logging, crash reporting,
performing asynchronous tasks, routing, and more. Redux middleware is implemented as a chain of
functions that wrap the store's dispatch method. Each middleware receives
store's dispatch and getState functions as named arguments, and
returns a function.
Middleware is like a security guard or helper who stands between you and the toy box. When you send a message (action), the middleware can: 1) Read the message and write it down (logging), 2) Check if the message is allowed (validation), 3) Change the message before it goes through, 4) Do extra work like calling the internet (API calls), or 5) Stop the message completely if something's wrong!
// Logger middleware
const loggerMiddleware = store => next => action => {
console.group(action.type);
console.info('dispatching', action);
const result = next(action);
console.log('next state', store.getState());
console.groupEnd();
return result;
};
// Error handling middleware
const errorMiddleware = store => next => action => {
try {
return next(action);
} catch (err) {
console.error('Error in action:', action.type, err);
// Send to error tracking service
throw err;
}
};
// Add to store
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(loggerMiddleware)
.concat(errorMiddleware)
});
Immutability is the concept that data should not be changed after it's created. In Redux, state must be treated as immutable - you never modify the existing state object directly. Instead, you create a new state object with the desired changes. This is crucial for Redux's change detection mechanism, time-travel debugging, and React's rendering optimization. Redux Toolkit uses the Immer library internally to allow "mutating" syntax while maintaining immutability.
Immutability means "don't change the original". Imagine you have a drawing. Instead of erasing and changing the original drawing, you make a NEW drawing with the changes. This way, you can always go back and see what the old drawing looked like! In Redux, we never change the old state - we always make a new one with our changes.
// ❌ WRONG - Mutating state directly
function badReducer(state, action) {
state.count++; // Mutates original state!
state.todos.push(action.payload); // Mutates array!
return state; // Returns same reference
}
// ✅ CORRECT - Immutable updates (without Redux Toolkit)
function goodReducer(state, action) {
return {
...state, // Copy all properties
count: state.count + 1, // New value
todos: [...state.todos, action.payload] // New array
};
}
// ✅ BEST - Redux Toolkit with Immer
const slice = createSlice({
name: 'example',
initialState: { count: 0, todos: [] },
reducers: {
increment(state) {
state.count++; // Looks like mutation, but Immer handles it!
},
addTodo(state, action) {
state.todos.push(action.payload); // Immer makes it immutable!
}
}
});