State Management in React:- Context API and Redux Toolkit


Summary:
Prop drilling in React involves passing props through multiple component layers, complicating code management, especially in large applications. To avoid this, use solutions like Context API for global state sharing or state management libraries like Redux for complex state handling. Redux Toolkit simplifies Redux usage with features like `createSlice` and `configureStore`. Each has its best-use scenarios: Context API for simpler cases like theme switching, and Redux for larger applications needing frequent state updates.
Problem with Props- Prop Drilling
Prop drilling is a situation in React where you pass props through multiple levels of components to reach a deeply nested child component. This happens when intermediate components don't need the prop themselves but must pass it down to ensure the child gets it.
function App() {
const message = "Hello from App!";
return <Parent message={message} />;
}
function Parent({ message }) {
return <Child message={message} />;
}
function Child({ message }) {
return <p>{message}</p>;
}
Here, message
is passed from App
→ Parent
→ Child
, even though Parent
doesn't use it. This can make the code harder to manage. Because this example is simple, but in real applications we create larger components in separate files.
How to Avoid Prop Drilling?
Context API: Use React's Context to provide data to deeply nested components.
State Management Libraries: Redux, Zustand, or Recoil can help manage global state.
Context API
Let's implement dark mode using the Context API in React. We need a context to store the theme state (light/dark).
Step 1: Create the Context
Create a new file src/context/ThemeContext.js
:
import { createContext, useState } from "react";
// Create Context
export const ThemeContext = createContext();
// Provide Context
export const ThemeContextProvider = ({ children }) => {
const [darkMode, setDarkMode] = useState(false);
const toggleTheme = () => {
setDarkMode((prevMode) => !prevMode);
};
return (
<ThemeContext.Provider value={{ darkMode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
Saving Theme in localStorage:
import { createContext, useEffect, useState } from "react";
export const ThemeContext = createContext();
export const ThemeContextProvider = ({ children }) => {
const [darkMode, setDarkMode] = useState(
JSON.parse(localStorage.getItem("darkMode")) || false
);
const toggle = () => {
setDarkMode(!darkMode);
};
useEffect(() => {
localStorage.setItem("darkMode", darkMode);
}, [darkMode]);
return (
<ThemeContext.Provider value={{ darkMode, toggle }}>
{children}
</ThemeContext.Provider>
);
};
Step 2: Wrap the App with ThemeProvider
Now, wrap the App
component inside ThemeProvider
so all child components can access the theme.
Update index.js
(or main.jsx
):
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { ThemeContextProvider } from "./ThemeContext"; // Import the provider
ReactDOM.render(
<ThemeContextProvider>
<App />
</ThemeContextProvider>,
document.getElementById("root")
);
Step 3: Consume the Context in Components
Let's create a ThemeToggle
button to switch between dark and light modes.
Update ThemeToggle.js
:
import { useContext } from "react";
import { ThemeContext } from "./ThemeContext";
const ThemeToggle = () => {
const { darkMode, toggleTheme } = useContext(ThemeContext);
return (
<button onClick={toggleTheme}>
{darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"}
</button>
);
};
export default ThemeToggle;
Now we can use this button in any component to change Theme.
Redux Toolkit
Redux, React-Redux, Redux-Toolkit
Redux (State Management Library)
Redux is a state management library for JavaScript apps (mainly React). It stores your global state in a single source of truth (store) so components can access and update it without prop drilling.
Think of it like a centralized database for your app’s UI state.
React-Redux (Connecting Redux to React)
React-Redux is the official library that connects Redux to React. It provides:
Provider: Makes the Redux store available to all components.
useSelector: Access state from the Redux store.
useDispatch: Send actions to update the state.
Redux Toolkit (Simplified Redux)
Redux Toolkit (RTK) is the modern, recommended way to use Redux. It reduces boilerplate code and makes Redux easier with:
createSlice()
: Combines actions + reducers.configureStore()
: Sets up the Redux store with middleware.createAsyncThunk()
: Handles async API calls easily.
When to use redux and context API
Context API (For Simple State Management)
What it does: Allows sharing state globally across the app without prop drilling.
Best for: Small to medium applications where state updates are not complex.
Example Use Case: Theme switching, authentication state (user login/logout).
Redux (For Complex State Management)
What it does: Manages state in a centralized store with actions & reducers.
Best for: Large-scale applications with deeply nested components needing frequent state updates.
Example Use Case: E-commerce cart, real-time chat app, dashboards, data fetching.
Working with Redux Toolkit
https://redux-toolkit.js.org/introduction/getting-started
Step 1: Create React App
Step 2: Install Redux Toolkit and React-Redux packages
npm install @reduxjs/toolkit react-redux
Why Both Libraries?
Library | Purpose |
@reduxjs/toolkit | Manages state (store, reducers, actions, async logic) |
react-redux | Connects Redux store to React components |
✅ Redux Toolkit handles state
✅ React-Redux allows components to use that state
Folder Structure
Step 3: Set Up Redux Store
https://redux-toolkit.js.org/tutorials/quick-start
Create a file named src/app/store.js
.
import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice"; // Import feature reducer
export const store = configureStore({
reducer: {
counter: counterReducer, // Register reducer
}
});
The Redux store holds the global state of your application.
configureStore()
creates a store.We register the
counterReducer
inside thereducer
object.Now, Redux knows which part of the state belongs to which slice.
Step 4: Create a Feature Slice
features/counter/counterSlice.js
import { createSlice } from "@reduxjs/toolkit";
const counterSlice = createSlice({
name: "counter",
initialState: { value: 0 },
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
reset: (state) => { state.value = 0; }
}
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;
Each feature (like a counter, authentication, users) has its own slice.
createSlice()
generates a reducer & actions automatically.initialState
defines the starting state{ value: 0 }
.reducers
contain functions (mutations) to modify the state.Actions (
increment
,decrement
,reset
) are exported to be used in components.The reducer is exported to be registered in
store.js
.
Step 5: Provide Store in Main.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { store } from "./app/store"; // Import store from `app/`
import App from "./App";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
<Provider store={store}>
makes Redux available to all components.Without this, components won’t be able to access the store.
Step 6: Use Redux in Components
components/Counter.js
import { useSelector, useDispatch } from "react-redux";
import { increment, decrement, reset } from "../features/counter/counterSlice";
const Counter = () => {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<button onClick={() => dispatch(reset())}>Reset</button>
</div>
);
};
export default Counter;
useSelector(state => state.counter.value)
: Access Redux state.useDispatch()
: Gets the dispatch function to trigger actions.dispatch(increment())
: Calls the increment function from the slice.
How useSelector(state => state.counter.value)
works?
state
represents the entire Redux store.state.counter
refers to the counter slice inside the store.state.counter.value
extracts the actual counter value.Whenever
state.counter.value
changes, the component re-renders automatically.
Subscribe to my newsletter
Read articles from Omkar Kasture directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Omkar Kasture
Omkar Kasture
MERN Stack Developer, Machine learning & Deep Learning