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


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?
We define a global context using
createContext
, which holds two things:The current theme (
light
ordark
)A function to toggle the theme
The
ThemeProvider
component:Manages the actual
theme
state usinguseState
Provides
theme
andtoggleTheme
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.
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 theThemeProvider
.
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?
We use the
useTheme
hook that we created to fetch the theme.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.
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 withReact.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
WhileuseContext
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:
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.