Misusing React Context, Then Blaming React Context

Afifudin
·
16 July 2026

I’ve come across quite a few LinkedIn posts criticizing React Context.

Many of them make a claim like this:

“React Context causes every component to re-render, even components that don’t consume the context.”

That claim is simply incorrect.

Unfortunately, this misconception has spread widely, leading many developers to believe that React Context is inherently inefficient.

In this article, I’ll explain why those unexpected re-renders are usually caused by poor component composition, not by React Context itself.

The Most Common Mistake

Consider the following example:

const ThemeContext = createContext(null);
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Layout>
<Nav />
<Content />
<Footer />
</Layout>
</ThemeContext.Provider>
);
}

Suppose:

Yet Nav and Content still re-render whenever the theme changes. ⚠️

Many developers* immediately blame React Context.

*) at least based on what I’ve seen from LinkedIn posts

But React Context isn’t the problem here.

The problem is that the state lives inside App, and App is responsible for rendering Nav and Content.

Whenever theme state changes, App re-renders.
Since App renders Nav and Content, they naturally re-render as well—even though they never read from the context.

This is purely a composition problem.

A Better Composition

The fix is surprisingly simple.

Move the state into a dedicated provider component so that unrelated components are no longer rendered by the component that owns the state.

const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
function App() {
return (
<ThemeProvider>
<Layout>
<Nav />
<Content />
<Footer />
</Layout>
</ThemeProvider>
);
}

Now Nav and Content no longer re-render when the theme changes.

Why?

Because the state now lives inside ThemeProvider, and ThemeProvider only re-renders the ThemeContext.Provider. The children are passed through as-is rather than being recreated by the component that owns the state.

This is a much better composition and naturally avoids unnecessary re-renders.

Another Common Mistake: One Context for Everything

Another common mistake is putting unrelated state into a single context.

For example:

const AppContext = createContext(null);
function Providers({ children }) {
const [theme, setTheme] = useState('dark');
const [language, setLanguage] = useState('en');
const [user, setUser] = useState(null);
return (
<AppContext.Provider
value={{
theme,
setTheme,
language,
setLanguage,
user,
setUser,
}}
>
{children}
</AppContext.Provider>
);
}

Imagine a component that only reads the theme.

Changing the language still causes that component to re-render because the entire context value changes.

Unfortunately, useMemo doesn’t solve this.

Everything lives inside the same context, so every consumer receives the updated context value.

If these pieces of state are unrelated, they simply shouldn’t live in the same context.

Splitting Contexts Isn’t Enough

Some developers realize the previous problem and split the contexts.

const ThemeContext = createContext(null);
const LanguageContext = createContext(null);
const UserContext = createContext(null);
function Providers({ children }) {
const [theme, setTheme] = useState('dark');
const [language, setLanguage] = useState('en');
const [user, setUser] = useState(null);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<LanguageContext.Provider value={{ language, setLanguage }}>
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
</LanguageContext.Provider>
</ThemeContext.Provider>
);
}

At first glance this looks better.

But it still suffers from the same problem.
Whenever any state changes, the Providers component re-renders.

Since each provider creates a new object for its value prop, all three context values receive new object references. To avoid unnecessary updates, developers often end up memoizing every context value:

const ThemeContext = createContext(null);
const LanguageContext = createContext(null);
const UserContext = createContext(null);
function Providers({ children }) {
const [theme, setTheme] = useState('dark');
const [language, setLanguage] = useState('en');
const [user, setUser] = useState(null);
const themeValue = useMemo(() => ({ theme, setTheme }), [theme]);
const languageValue = useMemo(() => ({ language, setLanguage }), [language]);
const userValue = useMemo(() => ({ user, setUser }), [user]);
return (
<ThemeContext.Provider value={themeValue}>
<LanguageContext.Provider value={languageValue}>
<UserContext.Provider value={userValue}>
{children}
</UserContext.Provider>
</LanguageContext.Provider>
</ThemeContext.Provider>
);
}

This works.

But notice what happened, we had to add three separate useMemos just to compensate for the component structure.

Instead of fixing the symptom with memoization, we can eliminate the problem entirely by letting each provider own its own state.

Compose Providers Instead

A more natural solution is to let each provider own its own state.

const ThemeContext = createContext(null);
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
const LanguageContext = createContext(null);
function LanguageProvider({ children }) {
const [language, setLanguage] = useState('en');
return (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
);
}
const UserContext = createContext(null);
function UserProvider({ children }) {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
function App() {
return (
<ThemeProvider>
<LanguageProvider>
<UserProvider>
<Layout>
<Nav />
<Content />
<Footer />
</Layout>
</UserProvider>
</LanguageProvider>
</ThemeProvider>
);
}

Now each provider re-renders independently.

Changing the theme doesn’t recreate the language provider.
Changing the user doesn’t recreate the theme provider.

The render behavior becomes naturally efficient without relying on useMemo. Good composition often eliminates the need for optimization tricks.

”But There Are Too Many Providers!”

One complaint I often hear is:

“I don’t like wrapping my app with lots of providers.”

Personally, I don’t see a problem with that.
They’re just composition.

The important distinction is understanding the difference between:

These are not the same thing.

For example:

function App() {
return (
<Foo>
<Bar>
<Baz>
<Qux>
<Content />
</Qux>
</Baz>
</Bar>
</Foo>
);
}

Although this looks deeply nested, it isn’t creating a long render chain.

The render work is simply:

The children are merely passed through each component.

This is just composition.

Now compare it with this:

function App() {
return <Foo />;
}
function Foo() {
return <Bar />;
}
function Bar() {
return <Baz />;
}
function Baz() {
return <Qux />;
}
function Qux() {
return <Content />;
}

This creates an actual render chain.

The render flow becomes:

Now, if Foo re-renders, Bar also re-renders.
Then Baz. Then Qux. Then Content.

This is fundamentally different from passing elements as children.

HTML Nesting vs React Composition

Another misconception is treating React composition the same way as deeply nested HTML.

Deep HTML trees can absolutely affect performance.

More DOM nodes mean more work for the browser:

So reducing unnecessary HTML nesting can improve performance.

React composition is different.
Passing React elements as children is extremely cheap.

A deeply composed React tree doesn’t automatically translate into a deeply nested DOM tree, nor does it imply expensive rendering. Don’t confuse HTML nesting with React composition—they’re completely different concepts.

Final Thoughts

React Context isn’t the villain.

Most “React Context performance issues” are actually composition issues.

Instead of immediately reaching for useMemo, memo, or other optimizations, take a step back and look at your component structure.

A well-composed component tree naturally minimizes unnecessary re-renders.

#frontend #react