State Management
What is useContext?
useContext is a React hook that allows components to share and consume data (state)
without passing props down manually through every level of the component tree. We can
avoid prop drilling by:
1. Create a context (like a global store).
2. Provide that context value at a top level.
3. Any component inside can read the context without prop drilling.
Example:
import { createContext, useContext } from "react";
// 1. Create Context
const ThemeContext = createContext("light");
function App() {
const theme = "dark";
// 2. Provide Context to children
return (
<ThemeContext.Provider value={theme}>
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return (
<ThemeButton />
);
}
function ThemeButton() {
// 3. Consume Context value
const theme = useContext(ThemeContext);
return (
<button
style={{
backgroundColor: theme === "dark" ? "black" : "white",
color: theme === "dark" ? "white" : "black",
padding: "10px 20px",
}}
>
I am a {theme} themed button
</button>
);
}
export default App;
When NOT to use useContext
When you have a very complex state or lots of actions to update state.
When performance matters and you want to avoid unnecessary re-renders
(since useContext triggers re-renders for all consumers on every change).
When you need features like middleware, time-travel debugging, or advanced
devtools (Redux is better here).
What is Redux?
Redux is a predictable state container (predictable means => states are not mutated or
not updated directly but rather are re-assigned) for JavaScript apps (including React). It helps
you manage your app’s state in a single, centralized store and makes state changes
predictable and easier to debug.
Core Concepts
Concept What It Is & Why Important
Store The single source of truth holding all app state.
Actions Plain objects describing what happened (e.g., { type: 'INCREMENT' }).
Pure functions that calculate the new state based on the current state
Reducers
and an action.
Dispatch The method to send actions to the store to update state.
Subscriber
Components or code that listen for state changes.
s
How Redux works — the flow
1. You dispatch an action describing what happened.
2. The reducer function receives the current state and the action.
3. The reducer returns a new state based on the action.
4. The store updates the state and notifies subscribers (e.g., UI components).
5. UI updates according to the new state.
Getting Started with Redux
Redux is a JS library for maintainable global state management.
The whole global state of the app is stored in an object tree inside a
single store. The only way to change the state tree is to create an action, an object
describing what happened, and dispatch it to the store. To specify how state gets updated
in response to an action, we write pure reducer functions that calculate a new state based
on the old state and the action.
Redux Core Concepts
a) Store
The single source of truth for your app’s state.
Holds the entire state object.
Provides methods to get state, dispatch actions, and subscribe to changes.
b) Actions
Plain JavaScript objects that describe what happened in the app.
Must have a type property (string).
Can carry extra data via a payload property.
Example: { type: 'INCREMENT' } or { type: 'ADD_TODO', payload: 'Learn Redux' }.
c) Reducers
Pure functions that take the current state and an action, and return a new state.
Must not mutate the original state — always return a new object.
Example:
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT': return { count: state.count + 1 };
default: return state;
}
}
d) Dispatch
The method used to send actions to the store.
When you dispatch an action, Redux runs the reducer to update the state.
e) Selectors
Functions used to select parts of the state.
Helps separate state reading logic from components.
f) Subscribers
Functions or components that listen to the store to be notified of state changes.
Redux Toolkit (RTK) Concepts
Redux Toolkit is the official, recommended way to write Redux code that reduces
boilerplate and improves DX (developer experience).
a) configureStore()
A function that creates the Redux store with good defaults.
Automatically sets up:
o Redux DevTools integration
o Middleware like Redux Thunk for async actions
o Combines reducers easily
b) createSlice()
Combines reducers, actions, and action creators into one place.
Automatically generates action creators and types.
Uses Immer internally — lets you write reducers with “mutating” syntax safely.
c) createAsyncThunk()
Simplifies writing async logic (e.g., API calls).
Automatically generates lifecycle actions (pending, fulfilled, rejected).
Works well with extraReducers to handle async state changes.
d) Immer
A library used by RTK that allows writing mutating code in reducers while keeping
state immutable under the hood.
This simplifies reducer code a lot.
e) React-Redux Hooks
useSelector: Access Redux state in React components.
useDispatch: Get the dispatch function to send actions.
Hooks make React + Redux integration cleaner and easier than older HOCs or
connect().
f) Middleware
Redux middleware lets you intercept and handle actions.
RTK comes with Redux Thunk included by default, so you can dispatch async thunks
out of the box.
g) DevTools Integration
RTK automatically enables Redux DevTools Extension for easier debugging and time-
traveling state inspection.
Benefits of Redux Toolkit Over Plain Redux
Plain Redux Redux Toolkit (RTK)
Lots of boilerplate code Minimal boilerplate, simple API
Manual action types & creators createSlice auto-generates them
Manual store setup configureStore sets up store with defaults
Manual async handling Built-in support for thunks with
(middleware) createAsyncThunk
Manual immutable updates Immer simplifies immutable updates
Code Example:
// slice.ts
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: {
value: 0
},
reducers: {
add: state => {
state.value += 1
},
addByValue: (state, action) => {
state.value += action.payload;
},
sub: state => {
state.value -= 1
},
}
})
export const { add, sub, addByValue } = counterSlice.actions;
export { counterSlice };
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import { counterSlice } from "./slice";
const store = configureStore({
reducer: {
counter: counterSlice,
// user: userSlice - write as many slices as you want, configureStore automatically
combines them
});
export { store };
export type RootState = ReturnType<typeof store.getState>;
// App.tsx
import { Provider } from "react-redux";
import { store } from "./store";
import MainPage from "./MainPage";
const App = () => {
return (
<Provider store={store} >
<MainPage />
</Provider>
)
}
export default App;
// MainPage.tsx
import { useDispatch, useSelector } from "react-redux";
import { add, addByValue, sub } from "./slice";
import type { RootState } from "./store";
const MainPage = () => {
const value = useSelector((state: RootState) => state.counter.value);
const dispatch = useDispatch();
return (
<>
<div>MainPage value = {value}</div>
<button onClick={() => dispatch(add())}>Add</button>
<button onClick={() => dispatch(sub())}>Sub</button>
<button onClick={() => dispatch(addByValue(5))}>Add by 5</button>
</>
)
}
export default MainPage;
Middleware’s in Redux
Middleware in Redux is a function that runs between dispatching an action and
reaching the reducer.
It lets you:
Intercept actions.
Add logic like logging, async calls, conditionally dispatching new actions.
Modify, delay, or cancel actions.
They are because:
1. Reducers must be pure and synchronous.
2. Middleware handles side effects like:
API calls
Logging
Delaying actions
Condition-based dispatching
Simple Middleware Flow
dispatch(action)-- > middleware-- > reducer-- > store updated
Creating a simple custom middleware
// logger.ts
import type { Middleware } from "@reduxjs/toolkit";
export const loggerMiddleware: Middleware = (storeAPI) => (next) => (action)=>{
console.log("Dispatching = ", action);
const result = next(action);
console.log("Next State = ", storeAPI.getState());
return result;
};
// update store to this
// store.ts
import { configureStore } from "@reduxjs/toolkit";
import { counterSlice } from "./slices/counterSlice";
import { userSlice } from "./slices/userSlice";
import { loggerMiddleware } from "./middlewares/logger";
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
user: userSlice.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
});
export { store };
export type RootState = ReturnType<typeof store.getState>;
// then using any reducers would run this middleware
createAsyncThunk is a helper function from Redux Toolkit that simplifies handling
asynchronous operations (e.g., API requests) in Redux.
It automatically creates three lifecycle action types:
pending – when the async request is initiated.
fulfilled – when the request completes successfully.
rejected – when the request fails (e.g., network error).
This reduces boilerplate and ensures a consistent pattern for managing async state (loading,
success, error).
Simple Example by Fetching User
// features/user/userSlice.ts
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
interface User {
id: number;
name: string;
email: string;
}
interface UserState {
user: User | null;
loading: boolean;
error: string | null;
}
const initialState: UserState = {
user: null,
loading: false,
error: null,
};
export const fetchUser = createAsyncThunk("user/fetchUser", async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
return response.json() as Promise<User>;
});
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(fetchUser.pending, (state) => {
state.loading = true;
state.error = null;
state.user = null;
})
.addCase(fetchUser.fulfilled, (state, action: PayloadAction<User>) => {
state.loading = false;
state.user = action.payload;
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || "Failed to fetch user";
});
},
});
export default userSlice.reducer;
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
import userReducer from "./slices/userSlice";
export const store = configureStore({
reducer: {
user: userReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
import React from “react”;
import { useDispatch, useSelector } from “react-redux”;
import { fetchUser } from “./slices/userSlice”;
import type { RootState, AppDispatch } from ”./store”;
function App() {
const dispatch: AppDispatch = useDispatch();
const { user, loading, error } = useSelector((state: RootState) => state.user);
return (
<div style={{ padding: '2rem' }}>
<h1>Fetch User with createAsyncThunk</h1>
<button onClick={() => dispatch(fetchUser())}>Fetch User</button>
{loading && <p>Loading...</p>}
{error && <p style={{ color: 'red' }}>❌ {error}</p>}
{user && (
<div>
<p>👤 Name: {user.name}</p>
<p>📧 Email: {user.email}</p>
</div>
)}
</div>
);
}
export default App;
Redux State
Action UI Output
Change
fetchUser.pendi
Show "Loading..." loading: true
ng
fetchUser.fulfille Show user user set, loading:
d name/email false
fetchUser.reject Show error error set, loading:
ed message false
React-Redux Hooks
React Redux provides special hooks to access the Redux store and dispatch actions in
function components
Hook Description
useSelector Read data from the Redux store
useDispatch Send actions to the Redux store
useStore (less Access the underlying Redux store object
common) directly