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
- The Context Superpower
- Step 1 — Create the Context
- Step 2 — Provide a Value
- Step 3 — Consume with
useContext
- A Bigger Example
- The Re-Render Problem
-
Enter
useContextSelector
in React 19 -
useContextSelector
in Action -
Mixing
useContext
anduseContextSelector
- Best Practices and Gotchas
- Wrapping It All Up
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>;
}
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>;
}
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();
- 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>
);
}
- 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>;
}
- 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>
);
}
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:
- The current logged-in user
- 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>
);
}
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>);
}
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);
-
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>);
}
What happens now:
-
UserProfile
only re-renders whencurrentUser
changes. -
MessageList
only re-renders whenmessages
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:
- Using classic
useContext
- 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>
);
}
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>);
}
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>);
}
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
...
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>
);
}
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>;
}
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
-
Start with
useContext
for simplicity. - Profile your app.
-
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);
Bad (changes data during selection):
const currentUser = useContextSelector(ChatContext, v => {
v.currentUser.lastSeen = Date.now(); // ❌ mutation
return v.currentUser;
});
✅ 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
Good:
const theme = useContextSelector(SettingsContext, v => v.theme);
// Simple primitive value — stable unless it actually changes
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