DEV Community

Cover image for useContext in React — Why It Exists and How to Use It Simply
DHANRAJ S
DHANRAJ S

Posted on

useContext in React — Why It Exists and How to Use It Simply

Hey!

Before we start — let me show you something.

You have a React app. You want to show the logged-in user's name in three places - the Navbar, the Sidebar, and the Profile page.

So you store the name in the top-level component and pass it down.

function App() {
  const user = "Ravi";

  return <Navbar user={user} />;
}
Enter fullscreen mode Exit fullscreen mode

Simple enough. But then Navbar does not use user directly. It just passes it to another component inside it.

function Navbar({ user }) {
  return <UserMenu user={user} />;
}
Enter fullscreen mode Exit fullscreen mode

And UserMenu passes it further down.

function UserMenu({ user }) {
  return <p>Welcome, {user}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Navbar received user — but never used it. It just passed it along like a postman delivering a package it has no interest in.

Now imagine this happening across 5 levels of components.

That is the problem. And it has a name.


1. What Is Prop Drilling?

Prop drilling is when you pass data through multiple components just to get it to the one that actually needs it.

The components in the middle do not use the data. They just pass it down. Every. Single. Level.

App (has user data)
  ↓ passes user
  Navbar (does not need user — just passes it)
    ↓ passes user
    UserMenu (does not need user — just passes it)
      ↓ passes user
      Avatar (finally uses user here)
Enter fullscreen mode Exit fullscreen mode

Quick question for you.

What happens when you need to change the prop name from user to currentUser?

You have to update every single component in that chain. Navbar. UserMenu. Avatar. Every one of them.

For a small app — annoying. For a large app — a real nightmare.

That is exactly the problem useContext was built to fix.


2. What Is useContext?

useContext is a React hook that lets any component access data directly — without it being passed through props at every level.

You put the data in one place. Any component in your app can reach in and grab it. No passing. No middlemen.

Think of it like a noticeboard in an office.

Instead of the manager telling Team Lead, who tells the developer, who tells the intern about a policy change — the manager just pins it on the noticeboard. Anyone who needs it reads it directly.

useContext is that noticeboard.


3. Three Steps to Use useContext

useContext works in three steps. Every time. No exceptions.

Step 1 — Create the context

import { createContext } from "react";

const UserContext = createContext();
Enter fullscreen mode Exit fullscreen mode

This creates the noticeboard. It does not hold any data yet — it is just the container.

Step 2 — Provide the data

function App() {
  const user = "Ravi";

  return (
    <UserContext.Provider value={user}>
      <Navbar />
    </UserContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

UserContext.Provider wraps your components and gives them access to the data. The value prop is what you want to share.

Any component inside the Provider can now access user.

Step 3 — Consume the data

import { useContext } from "react";

function Avatar() {
  const user = useContext(UserContext);

  return <p>Welcome, {user}</p>;
}
Enter fullscreen mode Exit fullscreen mode

useContext(UserContext) reads the value from the Provider. Directly. No props needed. Avatar does not care how many components are above it — it just grabs the value straight from the context.


4. Fixing the Prop Drilling Problem

Remember the messy chain from the beginning? Let us fix it with useContext.

Before — with prop drilling:

function App() {
  const user = "Ravi";
  return <Navbar user={user} />;
}

function Navbar({ user }) {
  return <UserMenu user={user} />;
}

function UserMenu({ user }) {
  return <Avatar user={user} />;
}

function Avatar({ user }) {
  return <p>Welcome, {user}</p>;
}
Enter fullscreen mode Exit fullscreen mode

After — with useContext:

const UserContext = createContext();

function App() {
  return (
    <UserContext.Provider value="Ravi">
      <Navbar />
    </UserContext.Provider>
  );
}

function Navbar() {
  return <UserMenu />;
}

function UserMenu() {
  return <Avatar />;
}

function Avatar() {
  const user = useContext(UserContext);
  return <p>Welcome, {user}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Look at Navbar and UserMenu now. No more user prop. They do not know about it. They do not care.

Only Avatar — the component that actually needs the data — goes and gets it.

Clean. Direct. No middlemen.


5. Example 1 — Theme Toggle (Light and Dark Mode)

This is one of the most common real uses of useContext.

import { createContext, useContext, useState } from "react";

const ThemeContext = createContext();

function App() {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      <Page />
    </ThemeContext.Provider>
  );
}

function Page() {
  const { theme, setTheme } = useContext(ThemeContext);

  return (
    <div style={{
      background: theme === "dark" ? "#111" : "#fff",
      color: theme === "dark" ? "#fff" : "#111",
      padding: "30px"
    }}>
      <p>Current theme: {theme}</p>
      <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
        Toggle Theme
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The theme and setTheme are shared through context.

Any component inside the Provider — no matter how deep — can read the theme or change it. No prop drilling at all.

Quick question for you.

What would happen if you added a Navbar, Sidebar, and Footer — and all of them needed the theme?

Without context — you pass theme as a prop to all three. Then to their children. Then their children's children.

With context — they all just call useContext(ThemeContext) and get the theme directly. One line. Done.


6. Example 2 — Logged-In User Across Pages

Another very common use case. Once the user logs in — you want their name and role available everywhere.

import { createContext, useContext } from "react";

const AuthContext = createContext();

function App() {
  const loggedInUser = { name: "Ravi", role: "admin" };

  return (
    <AuthContext.Provider value={loggedInUser}>
      <Dashboard />
    </AuthContext.Provider>
  );
}

function Dashboard() {
  return (
    <div>
      <Header />
      <MainContent />
    </div>
  );
}

function Header() {
  const user = useContext(AuthContext);
  return <h2>Welcome, {user.name}</h2>;
}

function MainContent() {
  const user = useContext(AuthContext);
  return <p>You are logged in as: {user.role}</p>;
}
Enter fullscreen mode Exit fullscreen mode

Header and MainContent are both inside Dashboard. Neither of them received user as a prop.

But both of them read it directly from the context. No passing. No drilling.


7. When Should You Use useContext?

useContext is useful — but it is not for everything.

Use it when:

  • Data needs to be accessed by many components at different levels — theme, language, logged-in user, currency
  • You find yourself passing the same prop through 3 or more components just to get it somewhere
  • The data is truly global — something the whole app cares about

Do not use it when:

  • Only two components need the data — just pass a prop directly, it is simpler
  • The data changes very frequently — too many context updates can cause unnecessary re-renders
  • You just started and the app is still small — do not add complexity before you need it

Quick question for you.

You have a button and a modal on the same page. Clicking the button opens the modal. Should you use useContext for this?

Probably not. The button and modal are close to each other. A simple useState in the parent component and a prop to the modal is cleaner. useContext would be over-engineering for something this small.


8. useContext vs Prop Drilling — Side by Side

Prop Drilling useContext
How data moves Passed through every component Any component reads it directly
Middle components Must receive and pass the prop Do not need to know about it
Best for Data used by 1 or 2 nearby components Data needed across many levels
Code complexity Gets messy as app grows Stays clean regardless of depth
When to use Small, simple data flow Global data — theme, user, language

Quick Summary — 4 Things to Remember

  1. Prop drilling is the problem — passing data through components that do not need it, just to reach the one that does.

  2. useContext is the solution — any component inside the Provider can read the data directly. No middlemen.

  3. Three steps always — create the context with createContext(), wrap components with Provider, read the value with useContext().

  4. Do not overuse it — useContext is for truly global data. For simple cases between nearby components, a prop is still the right choice.


useContext is one of those things that feels unnecessary — until your app grows and prop drilling starts hurting.

Once you hit that wall — this is the hook you reach for.

Try building the theme toggle example above. Add a Navbar and a Footer that also read the theme from context. See how clean it feels compared to passing props everywhere.

If something did not click or you have a question — drop it in the comments below.


Thanks for reading. Keep building.

Top comments (0)