State Management in React:- Context API and Redux Toolkit

Omkar KastureOmkar Kasture
6 min read

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 AppParentChild, 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?

LibraryPurpose
@reduxjs/toolkitManages state (store, reducers, actions, async logic)
react-reduxConnects 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 the reducer 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?

  1. state represents the entire Redux store.

  2. state.counter refers to the counter slice inside the store.

  3. state.counter.value extracts the actual counter value.

  4. Whenever state.counter.value changes, the component re-renders automatically.


0
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