Redux & React-Redux: Complete Deep Dive π
Let me break this down from absolute basics to advanced, the way you'd explain it confidently in an interview.
π§ The Core Problem Redux Solves
Imagine a React app with 50 components. Component A needs data that lives in Component Z. Without Redux, you'd have to pass props down through every component in between β this is called prop drilling, and it's a nightmare.
Redux gives you a single global store β think of it as a big JavaScript object that any component can read from or write to directly.
ποΈ The 3 Sacred Principles of Redux
Single source of truth β the entire app state lives in ONE store object
State is read-only β you can NEVER directly mutate state (state.name = "John" β)
Changes are made with pure functions β those functions are called reducers
π Core Building Blocks
- Store The single JavaScript object holding your entire app's state. jsimport { createStore } from 'redux'; const store = createStore(rootReducer);
store.getState() // Read the current state
store.dispatch(action) // Send an action to change state
store.subscribe(fn) // Listen for state changes
- Action A plain JavaScript object that describes what happened. It MUST have a type field. js// Action { type: 'ADD_ITEM', payload: { id: 1, name: 'Laptop' } }
// Action Creator (a function that returns an action)
const addItem = (item) => ({
type: 'ADD_ITEM',
payload: item
});
Interview tip: Action is just a messenger β it says what happened, not how to handle it.
- Reducer A pure function that takes (currentState, action) and returns a new state. Never mutates the old state. jsconst initialState = { items: [], loading: false };
function cartReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] }; // NEW object!
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.payload) };
default:
return state; // Always return state in default!
}
}
Why pure? Same input β always same output. No side effects (no API calls, no random numbers). This makes Redux predictable and testable.
- Dispatch The only way to trigger a state change. jsstore.dispatch(addItem({ id: 1, name: 'Laptop' }));
π Connecting Redux to React (react-redux)
Provider
Wraps your entire app and makes the store available to all components.
jsximport { Provider } from 'react-redux';
root.render(
);
useSelector β Read from store
jsxconst items = useSelector((state) => state.cart.items);
// state = entire Redux store, you pick what you need
Interview tip: useSelector re-renders the component ONLY when the selected value changes. It's optimized.
useDispatch β Write to store
jsxconst dispatch = useDispatch();
const handleAdd = () => {
dispatch(addItem({ id: 1, name: 'Laptop' }));
};
Old way: connect() (you might be asked this)
jsxconst mapStateToProps = (state) => ({ items: state.cart.items });
const mapDispatchToProps = { addItem };
export default connect(mapStateToProps, mapDispatchToProps)(CartComponent);
Modern code uses hooks (useSelector/useDispatch), but legacy codebases still use connect.
βοΈ Middleware β The Most Important Advanced Topic
What is Middleware?
Middleware sits between dispatch and the reducer. It intercepts every action before it reaches the reducer.
dispatch(action) β [Middleware 1] β [Middleware 2] β Reducer β New State
This is where you handle side effects β API calls, logging, analytics, etc.
Middleware signature (this impresses interviewers):
jsconst myMiddleware = (store) => (next) => (action) => {
// Do something before
console.log('Before:', action);
next(action); // Pass to next middleware or reducer
// Do something after
console.log('After:', store.getState());
};
Applied like this:
jsimport { createStore, applyMiddleware } from 'redux';
const store = createStore(rootReducer, applyMiddleware(myMiddleware));
π₯ Redux Thunk (They WILL Ask This)
The Problem
By default, dispatch() only accepts plain objects. But what if you need to make an API call first, THEN dispatch?
jsdispatch(fetchUser()); // β fetchUser returns a function, not an object!
What Thunk Does
Redux Thunk is middleware that lets you dispatch a function instead of an object. That function receives dispatch and getState as arguments.
bashnpm install redux-thunk
jsimport thunk from 'redux-thunk';
const store = createStore(rootReducer, applyMiddleware(thunk));
Thunk in Action
js// This is a "thunk" β a function that returns a function
const fetchUsers = () => async (dispatch, getState) => {
dispatch({ type: 'FETCH_USERS_START' }); // Show loading spinner
try {
const response = await fetch('/api/users');
const data = await response.json();
dispatch({ type: 'FETCH_USERS_SUCCESS', payload: data }); // Save data
} catch (error) {
dispatch({ type: 'FETCH_USERS_ERROR', payload: error.message }); // Handle error
}
};
// Usage in component
dispatch(fetchUsers());
Interview answer for "What is Thunk?"
"Thunk is middleware that extends Redux's dispatch to accept functions. This lets us handle async operations like API calls. The thunk function receives dispatch and getState, so we can dispatch multiple actions β like a loading action, then a success or failure action β based on async results."
β‘ Redux Saga (The "Chunk" They Mentioned)
They likely said "Saga" not "chunk" β this is the other popular middleware.
Thunk vs Saga
Redux ThunkRedux SagaStyleAsync/await functionsGenerator functions (function*)ComplexitySimple, easy to learnSteeper learning curvePowerGood for simple asyncBetter for complex flowsTestingHarderEasier (pure effects)
Saga Basics
jsimport { call, put, takeEvery } from 'redux-saga/effects';
// Worker saga β does the actual work
function* fetchUsersSaga() {
try {
const data = yield call(fetch, '/api/users'); // call is like await
yield put({ type: 'FETCH_SUCCESS', payload: data }); // put is like dispatch
} catch (error) {
yield put({ type: 'FETCH_ERROR', payload: error });
}
}
// Watcher saga β watches for actions
function* watchFetchUsers() {
yield takeEvery('FETCH_USERS_REQUEST', fetchUsersSaga);
// takeEvery = run saga for every matching action
// takeLatest = only run for the most recent action (cancels previous)
}
Interview answer for "Thunk vs Saga?"
"Thunk is simpler β great for straightforward async operations. Saga uses generator functions and is better for complex scenarios like debouncing, cancelling requests, or running tasks in parallel. In most projects, Thunk is sufficient, but Saga shines in enterprise apps with complex async workflows."
π Redux Toolkit (RTK) β Modern Redux
This is how everyone writes Redux today. If you're not using RTK, you're writing too much boilerplate.
bashnpm install @reduxjs/toolkit
createSlice β Replaces actions + reducer
jsimport { createSlice } from '@reduxjs/toolkit';
const cartSlice = createSlice({
name: 'cart',
initialState: { items: [] },
reducers: {
addItem: (state, action) => {
state.items.push(action.payload); // RTK uses Immer β you CAN mutate here!
},
removeItem: (state, action) => {
state.items = state.items.filter(i => i.id !== action.payload);
}
}
});
export const { addItem, removeItem } = cartSlice.actions; // Auto-generated action creators
export default cartSlice.reducer;
Big deal: RTK uses Immer internally, so you can write "mutating" code that actually creates a new state. No more ...spread everywhere!
createAsyncThunk β Async with RTK
jsimport { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
// Creates the thunk + action types automatically
export const fetchUsers = createAsyncThunk(
'users/fetchAll', // action type prefix
async () => {
const res = await fetch('/api/users');
return res.json(); // This becomes action.payload on success
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { data: [], loading: false, error: null },
reducers: {},
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;
});
}
});
configureStore β Store setup
jsimport { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: {
cart: cartReducer,
users: usersReducer,
},
// RTK includes thunk middleware by default!
// Redux DevTools Extension enabled by default!
});
π Complete Data Flow (Say This in Interviews)
User clicks button
β
Component calls dispatch(action)
β
Middleware intercepts (Thunk runs async, then dispatches again)
β
Reducer receives (currentState, action) β returns newState
β
Store saves newState
β
useSelector detects change β component re-renders
π‘ Key Interview Answers
"Why Redux over Context API?"
Context re-renders ALL consumers when value changes. Redux is optimized β useSelector only re-renders when the specific piece of state changes. Also, Redux has DevTools, middleware support, and better structure for large apps.
"What is a pure function?"
A function that always returns the same output for the same input, and has no side effects (no API calls, no state mutation, no console logs).
"Can you have multiple reducers?"
Yes! Use combineReducers (or RTK's configureStore with a reducer object). Each reducer manages its own slice of state. They're combined into one root reducer.
"What is the Redux DevTools?"
A browser extension that lets you inspect every action dispatched, see the state before and after, and even time-travel debug β replay actions to reproduce bugs.
πΊοΈ Quick Mental Map
Redux = Store + Actions + Reducers
React-Redux = Provider + useSelector + useDispatch
Async = Thunk (simple) or Saga (complex)
Modern = Redux Toolkit (RTK) β use this always
Go through these concepts once more out loud and you'll walk into that interview owning the Redux topic. πͺ
Top comments (0)