DEV Community

Cover image for Avoiding Prop Drilling in React with useContext
Shefali
Shefali

Posted on • Originally published at shefali.dev

3

Avoiding Prop Drilling in React with useContext

In React applications, prop drilling is a common problem where data is passed down multiple levels through props, even when intermediate components don’t need it. This can make the code harder to maintain and update.

React’s Context API with the useContext hook provides a built-in solution to avoid prop drilling and simplify state management.

In this post, you’ll learn how to use context API with the useContext hook to avoid prop drilling.

Before we get started, don’t forget to subscribe to my newsletter!
Get the latest tips, tools, and resources to level up your web development skills delivered straight to your inbox. Subscribe here!

Now let’s jump right into it!🚀

What is Prop Drilling?

State management is important in React to keep different components in sync. In small applications, passing state from a parent component to a child component is simple. But as your app grows, you may need to share state between multiple components that are not related. This makes the process inefficient.

When state is passed at multiple levels inside a component, this is called prop drilling.

For example:

const Parent = () => {
  const user = { name: "John" };
  return <Child user={user} />;
};

const Child = ({ user }) => {
  return <GrandChild user={user} />;
};

const GrandChild = ({ user }) => {
  return <p>Hello, {user.name}!</p>;
};
Enter fullscreen mode Exit fullscreen mode

In this example, the user is passed down three levels, even though only GrandChild needs it.

👉 If you’re new to how state and props work in React, check out my beginner-friendly guide on State and Props before continuing.

How to Avoid Prop Drilling?

To avoid prop drilling, you can use React Context API.

React Context API is a built-in state management solution that provides a global store that you can access anywhere within the app and share state globally across components without passing props manually.

Context API is useful because:

  • It avoids prop drilling.
  • This is a lightweight alternative to Redux.
  • Needs no extra set up as it is built in React.
  • Works well for small to medium-sized applications.

How to Use React Context API (Step-by-Step Guide)

Here’s a step-by-step guide to setting up context API:

Step 1: Create a Context

Create a new file UserContext.js and define a context.

import { createContext } from "react";

// Create Context 
export const UserContext = createContext();
Enter fullscreen mode Exit fullscreen mode

Here,

  • createContext() creates a new global context.
  • This context will be used to share data between components.

Tip: Keep your context files in a separate folder within the app for code maintainability.

You might be wondering, “Do I need to memorize this Syntax?”

No worries! You can always refer back to this guide or the official docs whenever needed.

Step 2: Create a Context Provider

Create a context provider in the same file like this:

import { useState } from "react";

export function UserProvider({ children }) {
  const [user, setUser] = useState("John");

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

Here,

  • useState(“John”) initializes state with the value John.
  • <UserContext.Provider> wraps the app and provides the user state.
  • The value prop makes user and setUser available throughout the app.

Now, your UserContext.js file will look like this:

import { createContext, useState } from "react";

export const UserContext = createContext();

export function UserProvider({ children }) {
  const [user, setUser] = useState("John");

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

Step 3: Wrap the App in the Provider

Modify App.js to wrap the app with UserProvider.

import { UserProvider } from "./UserProvider";
import Home from "./Home";
import About from "./About";

function App() {
  return (
    <UserProvider>
      <Home />
      <About />
    </UserProvider>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Here,

  • The UserProvider wraps the entire application.
  • Now, Home and About can access the user state without prop drilling.

Step 4: Consume the Context in Components

(A) Read Context Data using useContext Hook

import { useContext } from "react";
import { UserContext } from "./UserContext";

function Home() {
  // The value of user is extracted using destructuring
  const { user } = useContext(UserContext);

  return <h1>Welcome, {user}!</h1>;
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

Here,

  • useContext(UserContext) gives access to the global state.
  • user is directly used in the component without passing props.

(B) Updating the State in Context

import { useContext } from "react";
import { UserContext } from "./UserContext";

function About() {
  const { user, setUser } = useContext(UserContext);

  return (
    <div>
      <h1>Current User: {user}</h1>
      <button onClick={() => setUser("David")}>Change User</button>
    </div>
  );
}

export default About;
Enter fullscreen mode Exit fullscreen mode

Here, clicking the button updates the user globally across all components.

Performance Optimization

When the context state gets updated, all of the connected components get re-rendered, which is unnecessary. To optimize this:

Use useMemo for Context Value

Wrap the state in useMemo to prevent re-renders unless the state actually changes.

const value = useMemo(() => ({ user, setUser }), [user]);
Enter fullscreen mode Exit fullscreen mode

Complete example:

export function UserProvider({ children }) {
  const [user, setUser] = useState("John");

  const value = useMemo(() => ({ user, setUser }), [user]);

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

Without useMemo, every re-render of the provider will create a new object for value={{ user, setUser }}; this will cause all the components (which are using the user state) to re-render even if the user hasn’t changed.

Split Contexts for Better Performance

Instead of using a single context for everything, create multiple contexts for different concerns (e.g., user state, theme state).

For example:

<UserProvider>
  <ThemeProvider>
    <App />
  </ThemeProvider>
</UserProvider>
Enter fullscreen mode Exit fullscreen mode

This prevents unnecessary updates when only one state changes.

How Does This Work?

  • UserProvider manages the user state (e.g., authentication, profile details).
  • ThemeProvider manages theme preferences (light/dark mode).
  • The App component and all its children can now access both user data and theme settings without prop drilling.

Creating Theme Context

import { createContext, useState } from "react";

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, any component inside <ThemeProvider> can access and update the theme.

When to Use Context API?

  • When multiple components are sharing the same state.
  • To avoid prop drilling.
  • To avoid heavy setup of Redux.
  • When you want to manage a global state with a lightweight solution.
  • For managing themes, authentication, or language settings.

🎯Wrapping Up

That’s all for today!

For paid collaboration connect with me at : connect@shefali.dev

I hope this post helps you.

If you found this post helpful, here’s how you can support my work:
Buy me a coffee – Every little contribution keeps me motivated!
📩 Subscribe to my newsletter – Get the latest tech tips, tools & resources.
𝕏 Follow me on X (Twitter) – I share daily web development tips & insights.

Keep coding & happy learning!

Neon image

Build better on Postgres with AI-Assisted Development Practices

Compare top AI coding tools like Cursor and Windsurf with Neon's database integration. Generate synthetic data and manage databases with natural language.

Read more →

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, cherished by the supportive DEV Community. Coders of every background are encouraged to bring their perspectives and bolster our collective wisdom.

A sincere “thank you” often brightens someone’s day—share yours in the comments below!

On DEV, the act of sharing knowledge eases our journey and forges stronger community ties. Found value in this? A quick thank-you to the author can make a world of difference.

Okay