React 19 `useContextSelector` Deep Dive — Precision State, Zero Wasted Renders
Ali Aslam

Ali Aslam @a1guy

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

Joined:
Aug 9, 2025

React 19 `useContextSelector` Deep Dive — Precision State, Zero Wasted Renders

Publish Date: Aug 21
1 0

React’s Context API is amazing for sharing state without prop-drilling…
…but sometimes, it’s a little too generous.

By default, useContext will happily tell your component every time anything in the context changes — even if you don’t care about that part.
That’s like getting a full weather report every time you just wanted to know if it’s sunny.

React 19’s useContextSelector fixes that, letting you subscribe only to the slice of state you actually need.
This deep dive will show you exactly how it works, when to use it, and how to avoid the sneaky pitfalls.


Table of Contents

Table of Contents

Table of Contents


Why Context Exists

Let’s start with a common React headache:
prop drilling — the “Here, hold this… and give it to the next guy” problem.

Picture this:

You’ve got a big app. At the top, you have an App component that knows the current theme (light or dark). Deep down — maybe 5, 6, or 7 components later — a little Button needs that theme.

How does it get there?
Without context, you pass it like this:

function App() {
  const theme = "dark";
  return <Page theme={theme} />;
}

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

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

function Content({ theme }) {
  return <Button theme={theme} />;
}

function Button({ theme }) {
  return <button className={theme}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

That’s a lot of “theme” props being dragged through components that don’t actually care about the theme. They’re just the middlemen.

This is prop drilling — and while it’s fine for small apps, in big apps it gets:

  • Repetitive
  • Error-prone
  • Annoying to maintain

The Context Superpower

Context says:

“Stop handing props down like hot potatoes.
I’ll store this value in a shared place,
and anyone in my ‘zone’ can grab it directly.”

Think of Context like a Wi-Fi network for values:

  • You connect to the Provider (router).
  • Any component inside the Provider’s range can “connect” and get the data.

Here’s the same theme example with Context:

const ThemeContext = React.createContext();

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

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

Now, the theme travels invisibly down the tree.
No more prop relays.


💡 Big Picture: Context is great for:

  • Global settings (theme, language)
  • Authenticated user info
  • App-wide configuration
  • Anything lots of components need access to

But — and this is key — useContext has a performance catch
…and that’s where React 19’s useContextSelector comes in.

We’ll talk about that next. 🚀


Step 1 — Create the Context

Think of this as creating your data channel.

const ThemeContext = React.createContext();
Enter fullscreen mode Exit fullscreen mode
  • This doesn’t hold the data yet — it just sets up a way for components to talk to each other.
  • We’ll fill it with data using a Provider.

Step 2 — Provide a Value

The Provider wraps the part of your app that should have access to the context value.

function App() {
  const theme = "dark";

  return (
    <ThemeContext.Provider value={theme}>
      <Page />
    </ThemeContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • The value prop here (theme) is what all children will “hear” when they connect.
  • Change the value, and all consumers re-render.
  • This “reactivity” is both the power and the gotcha of Context.

Step 3 — Consume with useContext

Anywhere inside that Provider, you can grab the value instantly.

function Button() {
  const theme = React.useContext(ThemeContext);
  return <button className={theme}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode
  • No prop drilling.
  • Reads are synchronous — you get the value immediately.
  • If theme changes, Button re-renders automatically.

A Bigger Example

Let’s say we store more than just theme:

const AppContext = React.createContext();

function App() {
  const [theme, setTheme] = React.useState("dark");
  const [user, setUser] = React.useState({ name: "Alex" });

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

function Header() {
  const { theme, user } = React.useContext(AppContext);
  return (
    <header className={theme}>
      Welcome, {user.name}
    </header>
  );
}
Enter fullscreen mode Exit fullscreen mode

Looks great, right?
Well… here’s the catch:

If any part of the value changes — say theme changes — every single consumer of AppContext re-renders, even if they only care about user.
That’s where performance can take a hit.


💡 Mental model:

  • useContext = “Give me the entire value from this Provider, and re-render me whenever any of it changes.”
  • Great for small, infrequently changing data.
  • Risky for large, frequently changing data.

The Re-Render Problem

At first glance, useContext feels perfect — no prop drilling, instant value access, automatic updates.
But under the hood, there’s a gotcha that can quietly chip away at your app’s performance.


The All-or-Nothing Update

When the value in your Context Provider changes…
React re-renders every single consumer of that context.

It doesn’t matter if the consumer only uses a tiny piece of the value — if anything changes, React treats it like the whole value has changed.


Example: The Chat App

Let’s say our context stores two things:

  1. The current logged-in user
  2. The list of chat messages
const ChatContext = React.createContext();

function ChatProvider({ children }) {
  const [currentUser, setCurrentUser] = React.useState({ name: "Alex" });
  const [messages, setMessages] = React.useState([]);

  return (
    <ChatContext.Provider value={{ currentUser, messages }}>
      {children}
    </ChatContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Now, in our app:

function UserProfile() {
  const { currentUser } = React.useContext(ChatContext);
  return <div>{currentUser.name}</div>;
}

function MessageList() {
  const { messages } = React.useContext(ChatContext);
  return messages.map((m) => <p key={m.id}>{m.text}</p>);
}
Enter fullscreen mode Exit fullscreen mode

Seems fine… until new messages start coming in.


The Problem in Action

Every time messages changes:

  • MessageList should re-render ✅
  • But UserProfile also re-renders ❌ …even though the current user hasn’t changed at all.

Why Does This Happen?

Because:

  • React compares the entire value object ({ currentUser, messages }) between renders.
  • If any property changes (like messages), the object reference changes.
  • That new object reference triggers re-renders for all consumers.

The Performance Impact

In small apps, this barely matters.
In large apps with:

  • Heavy rendering components
  • Frequent updates
  • Lots of consumers

…it can cause unnecessary work, dropped frames, and janky UI.


💡 Key takeaway:
useContext is “all-in” — you either subscribe to the entire value or you don’t.
If you only want one slice of that value without the rest triggering updates… you need a more precise tool.

Enter React 19’s useContextSelector — the scalpel instead of the sledgehammer.


Enter useContextSelector in React 19

React 19 brought a shiny new tool to the state-sharing toolbox:
useContextSelector.

If useContext is like saying:

“Give me everything in the bag, even if I only need one snack”

…then useContextSelector is like saying:

“Just give me the chips. Leave the rest in the bag.” 🥔

That one little shift changes everything about how context updates work.


The Problem It Solves

With useContext, your component gets the entire context value and subscribes to all of it.
So if any part of that value changes, your component re-renders — even if you never use the changed part.

useContextSelector flips that:

  • You choose the slice of context you want.
  • Your component re-renders only when that slice changes.

How It Works

The API looks like this:

const pieceYouWant = useContextSelector(Context, selectorFn);
Enter fullscreen mode Exit fullscreen mode
  • Context → The context you want to read from.
  • selectorFn → A function that takes the entire context value and returns just the piece you need.

React uses that selector to track your subscription.
If the selected value is shallowly equal to the previous one — no re-render.


Example: Chat App Redux

Here’s our earlier chat example, but with useContextSelector:

import { useContextSelector } from 'use-context-selector';

function UserProfile() {
  const currentUser = useContextSelector(ChatContext, v => v.currentUser);
  return <div>{currentUser.name}</div>;
}

function MessageList() {
  const messages = useContextSelector(ChatContext, v => v.messages);
  return messages.map(m => <p key={m.id}>{m.text}</p>);
}
Enter fullscreen mode Exit fullscreen mode

What happens now:

  • UserProfile only re-renders when currentUser changes.
  • MessageList only re-renders when messages changes.
  • Unnecessary re-renders? Gone.

Side-by-Side: useContext vs. useContextSelector

Feature useContext useContextSelector
What you get The entire value object A single slice of the value
When you re-render Any time any property changes Only when your selected slice changes
Setup complexity Simple Slightly more (selector function)
Best for Small, rarely changing values Large or frequently changing context data

Under the Hood

  • Think of useContextSelector like a custom subscription.
  • Instead of React saying, “Here’s the whole box — I’ll tell you if anything changes,” it says, “You only care about this piece, so I’ll only bother you if that piece changes.”
  • This avoids the ripple effect of unnecessary re-renders across unrelated components.

A Quick History

Before React 19, if you wanted this behavior, you had to install an external package called use-context-selector.
React 19 integrated that functionality natively — so no extra dependency needed.


Selector Best Practices

  • Keep it pure → Your selector should only read from the value and return something based on it — no side effects.
  • Return stable values → If your selector returns a new object/array every time, you’ll defeat the optimization (because the reference will always change).
  • Keep it simple → Avoid heavy computations inside selectors; compute those in the provider or elsewhere.

💡 Key takeaway:
useContext is “wake me up whenever anything changes.”
useContextSelector is “wake me up only when my thing changes.”


useContextSelector in Action

Enough theory — let’s actually see what useContextSelector does for performance.
We’ll build two versions of the same app:

  1. Using classic useContext
  2. Using useContextSelector

…and watch how many components re-render when only part of the data changes.


The Setup

We’ve got:

  • A UserProfile that only needs currentUser
  • A MessageList that only needs messages
  • A ChatProvider holding both values
const ChatContext = React.createContext();

function ChatProvider({ children }) {
  const [currentUser, setCurrentUser] = React.useState({ name: "Alex" });
  const [messages, setMessages] = React.useState([]);

  // Just simulating new messages every 2s
  React.useEffect(() => {
    const id = setInterval(() => {
      setMessages(m => [...m, { id: Date.now(), text: "New message" }]);
    }, 2000);
    return () => clearInterval(id);
  }, []);

  return (
    <ChatContext.Provider value={{ currentUser, messages }}>
      {children}
    </ChatContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Version 1 — Classic useContext

function UserProfile() {
  console.log("UserProfile rendered");
  const { currentUser } = React.useContext(ChatContext);
  return <div>{currentUser.name}</div>;
}

function MessageList() {
  console.log("MessageList rendered");
  const { messages } = React.useContext(ChatContext);
  return messages.map(m => <p key={m.id}>{m.text}</p>);
}
Enter fullscreen mode Exit fullscreen mode

Every time a new message arrives:

  • MessageList re-renders
  • UserProfile also re-renders (waste)

Version 2 — useContextSelector

import { useContextSelector } from 'use-context-selector';

function UserProfile() {
  console.log("UserProfile rendered");
  const currentUser = useContextSelector(ChatContext, v => v.currentUser);
  return <div>{currentUser.name}</div>;
}

function MessageList() {
  console.log("MessageList rendered");
  const messages = useContextSelector(ChatContext, v => v.messages);
  return messages.map(m => <p key={m.id}>{m.text}</p>);
}
Enter fullscreen mode Exit fullscreen mode

Now, when a message arrives:

  • MessageList re-renders
  • 🚫 UserProfile stays perfectly still (no wasted work)

The Payoff

In a dev console, the logs will tell the story:

UserProfile rendered
MessageList rendered
MessageList rendered
MessageList rendered
...
Enter fullscreen mode Exit fullscreen mode

Notice how UserProfile only renders once at the start.

This may seem small in our demo, but in a real app with dozens or hundreds of consumers,
you can save thousands of wasted renders.


💡 Why This Matters

  • Less wasted rendering = smoother UI
  • Great for frequently updated data (chat messages, notifications, real-time dashboards)
  • Still keeps the simplicity of Context without introducing a third-party state manager

Mixing useContext and useContextSelector

Just because useContextSelector is shiny and new doesn’t mean you should throw out useContext.
They actually play well together — you can mix and match depending on the situation.


When to Use useContext

  • For static or rarely-changing values
  • When you need the whole value object anyway
  • When simplicity matters more than micro-optimizations

Example:

function ThemeToggle() {
  const { theme, setTheme } = React.useContext(ThemeContext);
  return (
    <button onClick={() => setTheme(t => t === "dark" ? "light" : "dark")}>
      Toggle Theme (current: {theme})
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Themes don’t change every few milliseconds, so the performance win from useContextSelector would be negligible here.


When to Use useContextSelector

  • For frequently updated data
  • When components only need a small slice of the context
  • In performance-sensitive parts of your app (e.g., message lists, animations, dashboards)

Example:

function NotificationBadge() {
  const unreadCount = useContextSelector(NotificationContext, v => v.unreadCount);
  return <span>{unreadCount}</span>;
}
Enter fullscreen mode Exit fullscreen mode

Now the badge updates instantly without causing unrelated consumers to re-render.


Combining in the Same App

It’s perfectly fine to have:

  • Some components reading the whole value with useContext
  • Others selecting slices with useContextSelector

The provider doesn’t care — it just passes the value down.


A Strategic Approach

  1. Start with useContext for simplicity.
  2. Profile your app.
  3. Upgrade to useContextSelector in components that re-render too often.

That way you avoid premature optimization but still have the tool ready when you need it.


💡 Rule of thumb:
Don’t use useContextSelector everywhere “just because.”
Use it where it actually reduces unnecessary work.


Best Practices and Gotchas

useContextSelector is powerful — but like any sharp tool, you’ll want to handle it with care.
Let’s go over the do’s, the don’ts, and the “wait… what just happened?” moments.


✅ Do: Keep Selectors Simple and Pure

Your selector function should:

  • Only read from the context value
  • Return the part you care about
  • Avoid causing side effects

Good:

const currentUser = useContextSelector(ChatContext, v => v.currentUser);
Enter fullscreen mode Exit fullscreen mode

Bad (changes data during selection):

const currentUser = useContextSelector(ChatContext, v => {
  v.currentUser.lastSeen = Date.now(); // ❌ mutation
  return v.currentUser;
});
Enter fullscreen mode Exit fullscreen mode

✅ Do: Return Stable References

If your selector creates a new object or array each time, React will think it’s “different” and re-render anyway.

Bad:

const settings = useContextSelector(SettingsContext, v => ({ theme: v.theme }));
// New object every render — no optimization benefit
Enter fullscreen mode Exit fullscreen mode

Good:

const theme = useContextSelector(SettingsContext, v => v.theme);
// Simple primitive value — stable unless it actually changes
Enter fullscreen mode Exit fullscreen mode

If you must return an object, memoize it before returning.


⚠️ Don’t Over-Optimize Prematurely

  • Not every context needs useContextSelector.
  • Measure first — if re-renders aren’t hurting performance, stick with useContext for simplicity.

⚠️ Don’t Mix State Logic Into Selectors

Keep selectors focused — don’t add unrelated state reads or computations inside them.
Do that elsewhere to keep things predictable.


⚠️ Be Aware of Nested Contexts

If you’re using multiple contexts, you might mix useContext for one and useContextSelector for another — totally fine.
Just keep track of what each component depends on.


💡 Golden Rule:
useContextSelector is for precision subscriptions — treat it like a scalpel, not a sledgehammer.


Wrapping It All Up

We started this journey with the “bag of snacks” analogy — useContext gives you the whole bag, useContextSelector lets you pick just the snack you want.

Along the way, we saw:

  • How useContextSelector cuts down on unnecessary re-renders by letting components subscribe only to the data they need.
  • Side-by-side comparisons showing why this matters in apps with frequently changing state.
  • How to mix and match with useContext strategically, so you’re not over-engineering your state layer.
  • Best practices to avoid subtle bugs — like keeping selectors pure and returning stable references.

Your Mental Shortcut

  • Use useContext → when you need the whole value or updates are rare.
  • Use useContextSelector → when you need a specific slice and updates are frequent.

Think of it like subscribing to a newsletter:

  • useContext = “Send me everything you publish.”
  • useContextSelector = “Just send me the cat memes. I don’t care about the rest.”

With React 19, you no longer need a third-party package for this precision.
It’s built right in — so you can make your components faster, leaner, and smarter about what they listen to.


Next up in our series: React 19 useActionState Deep Dive — Async State Management Without the Boilerplate


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 0 total

    Add comment