The Art of React Hooks: Part 3 — Unlocking the Power of useContext for Cleaner State Management

Swapnil PantSwapnil Pant
5 min read

Hey guys! Here's the third blog of the React Hooks series to help you understand all the React hooks.
Today we will be covering up the useContext hook in detail.


Why useContext?

Before understanding what’s useContext we need to understand why useContext. Imagine a situation where you have a series of let’s say five nested components. Component A has a state variable which component E wants to use. The most basic solution would be to pass the state as prop through B, C, D before it finally reaches E. This is called prop drilling.

Sounds okay on paper, right? But this approach can cause some headaches — it may lead to unnecessary re-renders of B, C, and D, even though those components don’t actually need the state. Plus, it can make your code messy and harder to maintain.

import React, { useState, useEffect } from 'react';

const A = () => {
    //create a state variable num
    const [num, setNum] = useState(11);
    return <B num={num} />;
}

const B = ({ num }) => {
    return <C num={num} />;
}

const C = ({ num }) => {
    return <D num={num} />;
}

const D = ({ num }) => {
    return <E num={num} />;
}

const E = ({ num }) => {
    useEffect(() => {
        console.log(num);
    }, [num]);

    return null;
}

export default A;

So now what’s the solution? You got it right. It’s the useContext hook.


What’s useContext?

Now, let’s talk about what useContext actually is. It’s a React hook that helps solve the prop drilling problem. Basically, it lets you create a global-ish state (or shared data) that any component can access directly — no need to pass props through components that don’t actually need that data.


Let’s understand through an example code.

First Create a new React project along with TypeScript.

Create a new folder called context with a new file types.d.ts with the following code:

export interface ThemeContextType {
    theme: 'light' | 'dark';
    toggleTheme: () => void;
}

Then create a new file called themeContext.tsx with the following code.

import { createContext, useContext, useState } from 'react';
import type { ThemeContextType } from './types';

const ThemeContext = createContext<ThemeContextType | null>(null);

export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
    const [theme, setTheme] = useState<ThemeContextType['theme']>('light');

    const toggleTheme = () => {
        setTheme(theme === 'light' ? 'dark' : 'light');
    }

    return (
        <ThemeContext.Provider value={{ theme, toggleTheme }}>
            {children}
        </ThemeContext.Provider>
    )
}

// eslint-disable-next-line react-refresh/only-export-components
export const useTheme = () => {
    const context = useContext(ThemeContext);
    if (!context) {
        throw new Error('useTheme must be used within a ThemeProvider');
    }
    return context;
}

What’s Going On Here?

  1. We define a global context using createContext, which holds two things:

    • The current theme (light or dark)

    • A function to toggle the theme

  2. The ThemeProvider component:

    • Manages the actual theme state using useState

    • Provides theme and toggleTheme to all its child components through context

We’ll later wrap our root component (like App.tsx or main.tsx) with this ThemeProvider so that all nested components can access this context.

  1. The useTheme custom hook:

    • Gives you access to the context anywhere in your app

    • Also makes sure you’re not using it outside of the provider by throwing an error

children just refers to whatever components you pass inside the ThemeProvider.
It’s a way to say: "wrap all this stuff with the context."

Next create a new folder called components with 2 files ThemedBox.tsx and ThemeToggler.tsx

// ThemedBox.tsx
import { useTheme } from "../context/ThemeContext";

const ThemedBox = () => {
    const { theme } = useTheme();

    const styles = {
        backgroundColor: theme === 'light' ? 'white' : 'black',
        color: theme === 'light' ? 'black' : 'white',
        padding: '20px',
        borderRadius: '10px',
        margin: '20px',
        boxShadow: theme === 'light' ? '0 0 10px 0 rgba(0, 0, 0, 0.1)' : '0 0 10px 0 rgba(255, 255, 255, 0.1)',
        border: theme === 'light' ? '1px solid black' : '1px solid white',
    }

    return (
        <div style={styles}>
            This box is themed with {theme} theme.
        </div>
    )
}

export default ThemedBox;

What’s Going On Here?

  1. We use the useTheme hook that we created to fetch the theme.

  2. Then we wrote some custom styles according to the current theme and returned a div with those styles.

// ThemeToggler.tsx
import { useTheme } from "../context/ThemeContext";

const ThemeToggler = () => {
    const { theme, toggleTheme } = useTheme();

    const stylesForButton = {
        padding: '10px 20px',
        borderRadius: '5px',
        border: theme === 'light' ? '1px solid black' : '1px solid white',
        cursor: 'pointer',
        backgroundColor: theme === 'light' ? 'white' : 'black',
        color: theme === 'light' ? 'black' : 'white',
        margin: '20px',
    }

    return (
        <button style={stylesForButton} onClick={toggleTheme}>Toggle Theme</button>
    )
}

export default ThemeToggler;

What’s Going On Here?

We get both the theme and toggleTheme from the useTheme custom hook that we created, and create a button with styles according to the current theme, and on button’s click, we are toggling the theme from dark to light and then back to dark.


Here’s how the final example looks like.

Theme Toggle Demo


Limitations of useContext.

Now before you go and start using useContext everywhere, let’s talk about some of its limitations.

  • Not ideal for frequent updates
    If your context value is something that updates very frequently (like mouse position or rapidly changing data), it can cause unnecessary re-renders of all components that consume that context. That’s because any change in context causes all consumers to re-render.

  • No built-in memoization
    Unlike some state management libraries, useContext doesn’t optimize for performance out of the box. You’ll need to manually memoize values or wrap components with React.memo if needed.

  • Tight coupling
    Components using context are tightly coupled to it. This can make testing or reusability a bit trickier, especially if you want to reuse the same component in a different context or outside a provider.

  • Scales poorly for complex state
    While useContext is great for small, global-ish values like theme or user info, for more complex app-wide state management (like forms, API state, etc.), it's better to use state management libraries like Redux, Zustand, or Jotai.

  • Harder to track in large apps
    In bigger projects with multiple contexts, it can get confusing where a particular context is coming from or which provider is wrapping what. Managing nested providers becomes a bit messy.


You can find the code on:

https://github.com/swapn0652/Understanding_React_Hooks


I hope you learned something new today. You can reach out to me on:

  1. Twitter

10
Subscribe to my newsletter

Read articles from Swapnil Pant directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Swapnil Pant
Swapnil Pant

I'm a Software Engineer who loves challenges and is good at decision making. Skilled in HTML, CSS, JavaScript, Typescript, Node.JS, Express.JS, and React, with a strong attention to detail and problem-solving abilities, I'm passionate about staying ahead of industry trends and taking on new challenges.