React Context API & useContext Deep Dive (With Real-World Patterns & Pitfalls)
Ali Aslam

Ali Aslam @a1guy

About: https://www.linkedin.com/in/maliaslam/

Joined:
Aug 9, 2025

React Context API & useContext Deep Dive (With Real-World Patterns & Pitfalls)

Publish Date: Aug 17
0 1

Passing data in React can feel like running a relay race — every component grabs the prop baton just to hand it off again. 🏃‍♂️🏃‍♀️🏃

Sometimes that’s fine. But when the baton has to pass through half your app just to reach one little component at the bottom… well, that’s when the prop drilling monster starts showing up.

React’s useContext hook is our escape hatch.
In this deep dive, we’ll unpack why prop drilling gets messy, how context fixes it, and how to use it without falling into common traps — all with real-world examples and plenty of “aha!” moments.

Here’s what we’ll cover:

Table of Contents

Foundations

Working with Context in Practice

Advanced Techniques

Avoiding Mistakes

Wrapping Up


The “Prop Drilling” Problem

Let’s start with a story.

You’re working on a React app. There’s a User object at the top level — name, avatar, email. Deep down, five levels below, there’s a tiny Avatar component that needs the user.name.

You’re a good React citizen, so you do the obvious thing:
you pass user down as a prop from parent → child → grandchild → great-grandchild → Avatar.

It works. ✅

But then the product manager says: “Also, can you show the user’s name in the header, footer, and settings page?”

Suddenly, you’re threading that same prop through tons of components that don’t even care about user, just to get it to the ones that do.


Prop Drilling: The Whisper Game

Think of it like the old playground game “telephone” (or “whisper down the lane”):
You whisper a message to one person, they whisper it to the next, and by the time it reaches the end… it’s often changed or garbled.

In React’s case, it’s not that the data changes — it’s that the mechanics of passing it along are noisy, repetitive, and fragile.

Example:

function App() {
  const user = { name: "Alice" };
  return <Page user={user} />;
}

function Page({ user }) {
  return <Section user={user} />;
}

function Section({ user }) {
  return <Content user={user} />;
}

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

function Avatar({ user }) {
  return <h2>{user.name}</h2>;
}
Enter fullscreen mode Exit fullscreen mode

The problem:

  • Page, Section, and Content don’t care about user — they’re just middlemen.
  • If you rename user to currentUser, you have to update all the components in between.
  • Adding another prop for another deep child means repeating the process.

Why This Gets Messy Fast

  • Every new piece of data you need in a deep child adds more props to every intermediate component.
  • Refactoring is brittle — one missed rename breaks the chain.
  • Intermediate components become cluttered with “plumbing” code they don’t care about.

A Better Way

What if:

  • You could put user in a shared place.
  • Any component — no matter how deep — could just say “Hey React, give me the user” without bothering its parents.

That’s Context.

In the next section, we’ll break down exactly what Context is, how it works, and why it’s not just “global variables with extra steps.”


What Context Is (and Is Not)

Before we touch a single line of useContext code, you need the big picture.


The Mental Model: A Shared Box

Imagine there’s a magical shared box in your React app.
You put a value in the box at the top level, and any component underneath can open the box and read what’s inside — without anyone in between lifting a finger.

That’s Context.

The key pieces:

  • createContext(defaultValue) → creates the box.
  • <Provider value={…}> → puts something into the box.
  • useContext(Context) → opens the box and gets the current value.

But Wait — It’s Not State

Here’s where beginners often trip:
Context doesn’t store data by itself. It’s just the delivery system.

You still need state somewhere to hold changing values.

Think of it this way:

  • State is the data (the actual song you want to play).
  • Context is the radio station broadcasting the song to any listener in range.

If you change the song (state), the broadcast (context) updates for all listeners.


It’s Not a Global Variable, Either

Context can feel like a global because “any component can read it” — but it’s scoped.

  • Each <Provider> creates its own bubble of values.
  • You can have multiple independent contexts.
  • You can nest providers to override values in a section of the tree.

This scoping is what makes context safe to use in big apps — you can isolate values to only the parts of the tree that need them.


Default Values — The Fallback Station

When you call React.createContext(defaultValue), that defaultValue is what you’ll get if there’s no provider above your component in the tree.

Example:
You might create a ThemeContext with default value "light". If a component isn’t wrapped in <ThemeContext.Provider>, calling useContext(ThemeContext) will give "light".

This is super handy for:

  • Testing (you can skip providers for quick tests).
  • Providing reasonable fallbacks for optional context.

Recap

  • Context = a box you can fill at the top and open anywhere below.
  • State = the actual data; context just delivers it.
  • Providers = scoped broadcasts — they only affect the subtree they wrap.
  • Default values = fallbacks when there’s no provider.

The Basic API

Alright, let’s turn that “shared box” idea into real code.

React gives us three core steps when working with context:

  1. Create the context with React.createContext().
  2. Provide a value with <Context.Provider>.
  3. Consume that value with useContext(Context).

Step 1 — Create the Context

We start by creating a new context object:

import { createContext } from "react";

const ThemeContext = createContext("light"); // "light" = default value
Enter fullscreen mode Exit fullscreen mode
  • That "light" here is the default value.
  • If no provider is found above a component, this is what useContext will return.

Step 2 — Provide the Value

You wrap part of your component tree with the provider and pass a value prop.

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here:

  • Everything inside <ThemeContext.Provider> (in this case, <Page /> and all its descendants) will “see” the value "dark" when they read from the context.
  • If a component is outside this provider, it will get the default value ("light" in our example).

Step 3 — Consume the Value

Anywhere inside the provider, you can access the value with useContext.

import { useContext } from "react";

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>I’m a {theme} button</button>;
}
Enter fullscreen mode Exit fullscreen mode
  • useContext(ThemeContext) returns whatever value the nearest provider has — "dark" here.
  • This is not limited to direct children — it works at any depth.

Putting It All Together

Here’s a complete working example:

import { createContext, useContext } from "react";

const ThemeContext = createContext("light");

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>I’m a {theme} button</button>;
}

function Page() {
  return (
    <div>
      <Button />
    </div>
  );
}

export default function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Result:

  • The button will render with the "dark" theme because it’s inside the provider.
  • If we moved <Button /> outside <ThemeContext.Provider>, it would fall back to "light".

Key Things to Remember

  • useContext always finds the nearest provider above the component.
  • No provider? You get the default value from createContext().
  • Providers are just React components — you can nest them, move them around, and even have multiple different contexts.

Where the Value Comes From

One of the coolest — and most misunderstood — things about context is how React decides which value you get when you call useContext.
It’s not magic — it’s all about providers and the component tree.


Rule #1 — Nearest Provider Wins

When you call:

const theme = useContext(ThemeContext);
Enter fullscreen mode Exit fullscreen mode

React doesn’t just grab the first provider it finds anywhere in the app — it walks up the component tree from your component, and stops at the nearest <ThemeContext.Provider>.

Think of it like climbing up a ladder:

  • The first provider you bump into? That’s the one you get your value from.
  • If you never find one, you use the default value from createContext().

Example: Single Provider

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
    </ThemeContext.Provider>
  );
}

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>{theme} button</button>;
}
Enter fullscreen mode Exit fullscreen mode

Result: "dark button". Easy.


Example: Nested Providers

Providers don’t have to be at the very top of the app — you can put them anywhere, even inside each other.

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Page />
      <ThemeContext.Provider value="light">
        <SpecialSection />
      </ThemeContext.Provider>
    </ThemeContext.Provider>
  );
}

function Button() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>{theme} button</button>;
}
Enter fullscreen mode Exit fullscreen mode

Here’s what happens:

  • <Page /> is inside the outer provider → all Buttons inside it get "dark".
  • <SpecialSection /> is inside its own provider with value "light" → all Buttons inside here get "light", even though there’s a "dark" provider above it.

Why This is Powerful

This “nearest provider wins” rule means:

  • You can override context for just part of your app.
  • You can scope context values to smaller parts of the tree.
  • You avoid the downsides of truly global variables (where changes affect everything).

Example uses:

  • A specific section of your app uses a different theme.
  • A form wizard has its own step-tracking context, separate from the global one.

Rule #2 — Default Value if No Provider

If React climbs all the way up the tree and doesn’t find a provider, it uses the defaultValue from when you called createContext().

Example:

const ThemeContext = createContext("light");

function OrphanButton() {
  const theme = useContext(ThemeContext);
  return <button className={theme}>{theme} button</button>;
}
Enter fullscreen mode Exit fullscreen mode

No provider anywhere → theme is "light".


Checkpoint:

  • useContext is scoped — it’s not “read from anywhere, anytime”.
  • The closest provider above your component in the tree decides the value.
  • No provider? You get the default value from createContext().

Updating Context

So far, our context values have been static — like "dark" or "light".
That’s fine for constants… but real apps have moving parts.

What if:

  • A user clicks a “Toggle Theme” button?
  • An admin logs in and you want to broadcast new user info?

We need our context value to change over time.


Key Idea

Context itself doesn’t hold state — but you can pass anything as the value, including:

  • A state variable
  • A state setter function
  • An object containing both

When that state changes, all components reading from that context will re-render.


Example: Theme Toggle Context

Let’s build a theme toggle where any component can:

  1. Read the current theme
  2. Switch to the other theme
import { createContext, useContext, useState } from "react";

const ThemeContext = createContext();

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

  const toggleTheme = () =>
    setTheme((prev) => (prev === "light" ? "dark" : "light"));

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

function ThemedButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  return (
    <button className={theme} onClick={toggleTheme}>
      Current theme: {theme} (click to toggle)
    </button>
  );
}

export default function App() {
  return (
    <ThemeProvider>
      <ThemedButton />
      <ThemedButton />
    </ThemeProvider>
  );
}
Enter fullscreen mode Exit fullscreen mode

How It Works

  • ThemeProvider owns the state (theme) and a setter (toggleTheme).
  • It passes both through the context value.
  • Any component can:

    • Read theme
    • Call toggleTheme to update it
  • When the state changes, React re-renders:

    • The provider
    • Every consumer that uses this context

Pattern: Value as an Object

In this example, our value is an object { theme, toggleTheme }.
That’s common because:

  • You can pass multiple related things in one go.
  • It’s easier to add more context data later without changing all consumers.

But watch out:
Passing a new object on every render can cause unnecessary re-renders of consumers — we’ll address that in the Performance section.


Why Not Store State Inside Context?

Because context is not a storage mechanism — it’s a delivery mechanism.
State belongs in a provider (or somewhere above it).
That way, your provider can:

  • Update the state
  • Broadcast it to consumers
  • Control when and how it updates

Checkpoint:

  • Context doesn’t have to be static — pass state & setters to make it dynamic.
  • Any change to the value causes all consumers to re-render.
  • Wrapping state in a provider is the standard way to make dynamic context.

Performance Considerations

Here’s the thing about context:
When its value changes, every single consumer re-renders.
No “only the ones that actually use it” magic — if they call useContext with that context, they’re in the blast radius.


The Blast Radius Problem

Consider this:

<ThemeContext.Provider value={{ theme, toggleTheme }}>
  <Header />   {/* uses theme */}
  <Sidebar />  {/* doesn't care about theme */}
  <Footer />   {/* uses theme */}
</ThemeContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Even if only Header and Footer actually use theme, Sidebar will still re-render if it’s inside the provider — because the value prop has changed.

Why?
React compares the value prop of the provider by reference.
If you pass a new object every render (like { theme, toggleTheme }), React sees it as “different” every time — boom, re-renders.


How to Reduce Unnecessary Re-renders

There are a few common strategies:


1. Memoize the Context Value

Wrap the value in useMemo so React only creates a new object when one of its parts changes.

const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Now:

  • If theme stays the same, the object reference stays the same.
  • Consumers won’t re-render just because the provider rendered.

2. Split Contexts

If your context holds unrelated data, split it into multiple contexts so changes to one don’t affect consumers of the other.

Example:

const ThemeContext = createContext();
const AuthContext = createContext();
Enter fullscreen mode Exit fullscreen mode

A change to auth state won’t cause theme consumers to re-render.


3. Use Context Selectors (Advanced)

Libraries like use-context-selector let you pick only part of a context’s value — consumers re-render only when that part changes.

const theme = useContextSelector(ThemeContext, v => v.theme);
Enter fullscreen mode Exit fullscreen mode

This is more advanced and not built into React core (as of React 19), but useful in high-performance scenarios.


When to Care About Performance

  • If your provider wraps many consumers (hundreds/thousands)
  • If your context value changes frequently (every few ms or seconds)
  • If you notice wasted renders in dev tools

For most apps:

  • A few re-renders won’t kill performance.
  • Over-optimizing too early just makes your code harder to maintain.

Checkpoint:

  • Every value change = all consumers re-render.
  • useMemo is your first line of defense.
  • Split contexts when values are unrelated.
  • Only worry about selectors if you truly need high-performance updates.

useContext vs Prop Drilling vs Redux/Zustand

At this point, you might be wondering:

“If useContext is so handy, why not just use it for all my state and skip props entirely?”

Hold your horses. 🐎
While useContext solves some problems beautifully, it’s not a silver bullet.
Let’s see how it stacks up against the other common patterns.


Prop Drilling

When it’s fine:

  • Data is only needed 1–2 levels down.
  • The chain of components between source and target is short and unlikely to change much.

Pros:

  • Dead simple.
  • No extra abstractions — just pass props like normal.

Cons:

  • Gets messy when data must travel through many layers.
  • Intermediate components get cluttered with “plumbing” props they don’t care about.

useContext

When it’s great:

  • Shared state across multiple distant components.
  • Values that change infrequently or are read far more often than they’re written.

Pros:

  • Avoids prop drilling entirely.
  • Built into React — no extra dependencies.
  • Scoped by providers (you can have different values for different parts of the tree).

Cons:

  • Every change to the value re-renders all consumers.
  • Not ideal for rapidly changing data (e.g., animation frames, live cursor positions).
  • Debugging updates can be trickier if you forget where the value comes from.

Redux, Zustand, and Other Global State Libraries

When they shine:

  • Complex apps with lots of shared state that changes frequently.
  • Need for middleware, devtools, time-travel debugging, undo/redo.
  • State shared across unrelated parts of the UI without having to manage multiple providers.

Pros:

  • Often optimized for performance (only parts of the UI that need data re-render).
  • Rich tooling for debugging.
  • Predictable patterns for large teams.

Cons:

  • Extra dependency and learning curve.
  • More boilerplate than useContext in simple cases.

The Decision Cheat Sheet

Situation Best Fit
Just passing props down 1–2 levels Props
Need to share state across many distant components, changes infrequently useContext
Large app, complex state, frequent updates, need advanced debugging Redux/Zustand

Key takeaway:

  • Props are perfect for local, simple cases.
  • useContext is great for medium-scale shared state.
  • Global state libs are for high-scale complexity.

Choosing the right tool is less about “which is best” and more about “which fits this situation.”


Common Patterns

Now that you’ve got the theory, let’s talk real-world use cases.
These are the bread-and-butter scenarios where useContext really shines.


1. Theme / Locale Context

This is the classic “intro to context” example — sharing global UI preferences.

const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState("light");
  const toggleTheme = () =>
    setTheme((prev) => (prev === "light" ? "dark" : "light"));

  const value = useMemo(() => ({ theme, toggleTheme }), [theme]);

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

function ThemeToggleButton() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  return (
    <button onClick={toggleTheme}>
      Current theme: {theme}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why it works well:

  • Theme changes aren’t usually super frequent.
  • All parts of the app can easily respond to the theme.

2. Auth Context

Instead of passing user and logout props everywhere, stick them in context.

const AuthContext = createContext();

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = (newUser) => setUser(newUser);
  const logout = () => setUser(null);

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

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

function UserProfile() {
  const { user, logout } = useContext(AuthContext);
  return user ? (
    <>
      <h2>Hello, {user.name}</h2>
      <button onClick={logout}>Log out</button>
    </>
  ) : (
    <p>Please log in</p>
  );
}
Enter fullscreen mode Exit fullscreen mode

3. Configuration Context

For things like API base URLs, feature flags, or environment settings.

const ConfigContext = createContext({ apiUrl: "" });

function App() {
  const config = { apiUrl: "https://api.example.com" };
  return (
    <ConfigContext.Provider value={config}>
      <Dashboard />
    </ConfigContext.Provider>
  );
}

function Dashboard() {
  const { apiUrl } = useContext(ConfigContext);
  // Use apiUrl for fetch calls...
}
Enter fullscreen mode Exit fullscreen mode

Why it works well:

  • Configuration usually never changes at runtime.
  • Context makes it accessible anywhere without prop threading.

4. Data Fetching Context

Sometimes you have an API client or cache you want to make available globally.

const ApiClientContext = createContext();

function ApiClientProvider({ children }) {
  const client = useMemo(() => new ApiClient(), []);
  return (
    <ApiClientContext.Provider value={client}>
      {children}
    </ApiClientContext.Provider>
  );
}

function Products() {
  const api = useContext(ApiClientContext);
  // api.getProducts()...
}
Enter fullscreen mode Exit fullscreen mode

Pattern Recap

  • Theme / Locale → global UI settings, infrequent changes.
  • Auth → user state + login/logout actions.
  • Config → static app settings.
  • Data client → shared instances/services.

Advanced Context Techniques

Once you’ve mastered the basics, you’ll start running into situations where context needs a little… extra flair.
Here are some advanced tricks that make context more flexible and maintainable.


1. Dynamic Contexts (Scoped Context)

You don’t have to create all contexts at the top level of your app.
You can create a context inside a component to scope it to that part of the tree.

Example: a tab system where each tab group manages its own “active tab” state.

function TabGroup({ children }) {
  const TabContext = createContext();
  const [activeTab, setActiveTab] = useState(null);

  return (
    <TabContext.Provider value={{ activeTab, setActiveTab }}>
      {children}
    </TabContext.Provider>
  );
}

function Tab({ name }) {
  const { activeTab, setActiveTab } = useContext(TabContext);
  const isActive = activeTab === name;
  return (
    <button
      style={{ fontWeight: isActive ? "bold" : "normal" }}
      onClick={() => setActiveTab(name)}
    >
      {name}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Why it’s useful:

  • Multiple tab groups can exist independently.
  • No risk of “cross-talk” between groups.

2. Multiple Providers

Sometimes you have several contexts that all need to be available in a part of your app.
You can nest them — but it gets ugly fast:

<AuthContext.Provider value={auth}>
  <ThemeContext.Provider value={theme}>
    <ConfigContext.Provider value={config}>
      <App />
    </ConfigContext.Provider>
  </ThemeContext.Provider>
</AuthContext.Provider>
Enter fullscreen mode Exit fullscreen mode

Better: Create a Provider Composition Component.

function AppProviders({ children }) {
  return (
    <AuthContext.Provider value={auth}>
      <ThemeContext.Provider value={theme}>
        <ConfigContext.Provider value={config}>
          {children}
        </ConfigContext.Provider>
      </ThemeContext.Provider>
    </AuthContext.Provider>
  );
}

function Root() {
  return (
    <AppProviders>
      <App />
    </AppProviders>
  );
}
Enter fullscreen mode Exit fullscreen mode

This way your Root stays clean, and you only have to manage nesting in one place.


3. Consuming Multiple Contexts

If a component needs values from multiple contexts, you can just call useContext more than once:

const { user } = useContext(AuthContext);
const { theme } = useContext(ThemeContext);
Enter fullscreen mode Exit fullscreen mode

If you find yourself doing this a lot in multiple places, make a custom hook that wraps both:

function useAuthTheme() {
  return {
    ...useContext(AuthContext),
    ...useContext(ThemeContext),
  };
}
Enter fullscreen mode Exit fullscreen mode

4. Hiding Context Behind Custom Hooks

This is one of the most important maintainability tricks.
Instead of importing contexts everywhere, create a useSomething() hook that handles it for you:

function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}
Enter fullscreen mode Exit fullscreen mode

Advantages:

  • Centralizes the useContext call.
  • Lets you add error handling if used outside a provider.
  • Makes it easier to refactor context internals later without touching all consumers.

Checkpoint:

  • Context doesn’t have to be global — you can scope it dynamically.
  • Multiple providers can be neatly composed into one.
  • Consuming multiple contexts can be simplified with custom hooks.
  • Wrapping useContext in a hook improves maintainability and safety.

Pitfalls & Gotchas

Context is powerful, but — like a power tool — it can make a mess if you don’t handle it carefully.
Here are the common traps that even experienced React devs fall into.


1. Overusing Context

If you find yourself thinking, “I’ll just put everything in context!” — stop.
Context is not a replacement for props, and overusing it can:

  • Make dependencies between components less obvious.
  • Hide where data is coming from.
  • Cause unnecessary re-renders.

Rule of thumb:
If a value is only needed in 1–2 components, just pass it as a prop.


2. Frequent Value Changes

Remember: any change to the context value re-renders all consumers.
If the value changes very frequently (e.g., on every mouse move), you might tank performance.

Fix:

  • Use local state for frequently changing bits.
  • Or split contexts so fast-changing values don’t affect unrelated consumers.

3. New Object / Function Every Render

If you pass an object or function directly as value, it’s a new reference every render → triggers consumer re-renders even if nothing inside changed.

<ThemeContext.Provider value={{ theme, toggleTheme }}>
Enter fullscreen mode Exit fullscreen mode

Fix: Wrap in useMemo or useCallback.

const value = useMemo(() => ({ theme, toggleTheme }), [theme]);
<ThemeContext.Provider value={value}>
Enter fullscreen mode Exit fullscreen mode

4. Consuming Outside a Provider

If you call useContext without a provider, you’ll get the default value — which might not be what you expect.

Fix:
Wrap your useContext call in a custom hook that throws a helpful error if there’s no provider above it.


5. Forgetting Context Scope

Providers are scoped. If you create a new provider further down the tree, it overrides any above it — which can lead to “why is my value wrong?” moments.

Always double-check your provider placement in the tree.


6. Putting Business Logic Inside Context

It’s tempting to shove all your state-changing logic directly in your provider…
But too much logic in one place can make the provider huge and hard to maintain.

Better: Keep providers focused on delivery.
Extract business logic into hooks or separate state machines, and pass only what consumers need.


Checkpoint:

  • Context is a scalpel, not a hammer — don’t overuse it.
  • Watch out for unnecessary re-renders from reference changes.
  • Scope and provider placement matter.
  • Keep business logic separate for cleaner code.

Wrap-Up & Cheat Sheet

If you’ve made it this far, you’re now officially context-aware — in the React sense, at least.
Let’s tie it all together so you can leave with both the mental model and the practical checklist.


The Mental Model

  • Context is like a shared box you put at the top of a React tree.
  • A provider puts something in the box.
  • Any component inside can open the box (useContext) and get the latest value.
  • The value is scoped — the nearest provider wins.

Core Steps

  1. Create:
   const MyContext = createContext(defaultValue);
Enter fullscreen mode Exit fullscreen mode
  1. Provide:
   <MyContext.Provider value={something}>
     {children}
   </MyContext.Provider>
Enter fullscreen mode Exit fullscreen mode
  1. Consume:
   const value = useContext(MyContext);
Enter fullscreen mode Exit fullscreen mode

When to Use Context

✅ Avoiding prop drilling across many layers.
✅ Sharing state across distant parts of the app.
✅ Values that don’t change every few milliseconds.


When Not to Use Context

❌ Data only needed in a couple of components — just use props.
❌ Very frequently changing values (e.g., animation state) — can cause re-render storms.
❌ Complex state management that needs advanced tooling — use Redux, Zustand, or similar.


Performance Checklist

  • Wrap objects/functions in useMemo/useCallback.
  • Split contexts when values are unrelated.
  • Avoid overloading one provider with everything.

Common Patterns

  • Theme / Locale Context — global UI settings.
  • Auth Context — user state + login/logout actions.
  • Config Context — static app settings.
  • Data Client Context — shared API clients or services.

The One-Sentence Takeaway

Context is your go-to for sharing state without the prop-passing relay race — but like any strong tool, it works best when you use it just enough, not everywhere.


Final Words:
Next time you find yourself endlessly passing a prop through a chain of components that don’t care about it, pause and think:

“Maybe this belongs in context.”

And if you also think:

“Maybe I’m overcomplicating this…”

You’re already ahead of the game.


👉 Coming up next: React 19 useMemo Explained How to Make React Remember Stuff (and When Not To)


Follow me on DEV for future posts in this deep-dive series.
https://dev.to/a1guy
If it helped, leave a reaction (heart / bookmark) — it keeps me motivated to create more content
Want video demos? Subscribe on YouTube: @LearnAwesome

Comments 1 total

  • Janidu Heshan
    Janidu HeshanAug 17, 2025

    📢 Play Games & Earn Money for FREE!

    Want to make money without spending a single penny?
    ✅ FREE Sign - Up Bonus : Get $10 instantly when you sign up !
    🎯 No investments need - just play and start earning today !
    Sign up :sljobsrilankadotcom.blogspot.com/2...

Add comment