Questions
An expensive component is re-rendering unnecessarily. How would you fix this state management issue?
The Scenario
A junior developer built a new feature using React Context to manage the application’s global state. The state object contains everything: the current user’s info, and the UI theme (light/dark mode).
They’ve noticed a performance problem. The UserWelcome component, which is expensive to render, re-renders every single time the theme is changed. This is making the UI feel sluggish.
The Challenge
You’ve been given the code that demonstrates this issue.
- Explain why the
UserWelcomecomponent re-renders every time the “Toggle Theme” button is clicked. - Describe two different strategies to fix this performance issue so that
UserWelcomeonly re-renders when theuserdata it cares about actually changes.
import React, { useState, useContext, createContext } from 'react';
// The monolithic context holding all global state
const AppContext = createContext();
// A component that is "expensive" to render
function UserWelcome() {
const { user } = useContext(AppContext);
console.log('Re-rendering UserWelcome (expensive)...');
return <h1>Welcome, {user.name}!</h1>;
}
// A component to change the theme
function ThemeToggler() {
const { theme, setTheme } = useContext(AppContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}
// The main App provider
export default function App() {
const [user, setUser] = useState({ name: 'Alice' });
const [theme, setTheme] = useState('light');
const contextValue = { user, setUser, theme, setTheme };
return (
<AppContext.Provider value={contextValue}>
<div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff', padding: '20px' }}>
<ThemeToggler />
<hr />
<UserWelcome />
</div>
</AppContext.Provider>
);
} The Explanation: The Problem with Monolithic Context
How Different Experience Levels Approach This
[object Object]
[object Object]
- Context over facts: Explains when and why, not just what
- Real examples: Provides specific use cases from production experience
- Trade-offs: Acknowledges pros, cons, and decision factors
The Performance Issue
The performance issue is a classic pitfall of using React Context. When a component consumes a context (with useContext), it subscribes to all changes in that context’s value.
In this code, we have a single, monolithic AppContext that holds both user and theme. The UserWelcome component only needs user, but because it’s subscribed to the entire AppContext, React will re-render it whenever any value in the context provider’s value object changes. Clicking “Toggle Theme” creates a new contextValue object with a new theme, triggering a re-render in every component that consumes AppContext, including the expensive UserWelcome.
Here are two ways to solve this:
Solution 1: Split the Contexts (Best Practice)
The most robust solution is to separate unrelated state into different contexts. Components can then subscribe only to the state they care about.
import React, { useState, useContext, createContext } from 'react';
// Create two separate contexts
const ThemeContext = createContext();
const UserContext = createContext();
function UserWelcome() {
// Subscribe ONLY to the UserContext
const { user } = useContext(UserContext);
console.log('Re-rendering UserWelcome (expensive)...');
return <h1>Welcome, {user.name}!</h1>;
}
function ThemeToggler() {
// Subscribe ONLY to the ThemeContext
const { theme, setTheme } = useContext(ThemeContext);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
);
}
export default function App() {
const [user, setUser] = useState({ name: 'Alice' });
const [theme, setTheme] = useState('light');
// Nest the providers
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<UserContext.Provider value={{ user, setUser }}>
<div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff', padding: '20px' }}>
<ThemeToggler />
<hr />
<UserWelcome />
</div>
</UserContext.Provider>
</ThemeContext.Provider>
);
}
Now, when the theme changes, only the ThemeContext.Provider gets a new value, and only its consumers (ThemeToggler and the div) will re-render. UserWelcome is unaffected.
Solution 2: Memoization
A quicker, but often more brittle, solution is to use React.memo to wrap the expensive component. React.memo is a higher-order component that prevents a component from re-rendering if its props haven’t changed.
// Wrap the expensive component in React.memo
const MemoizedUserWelcome = React.memo(function UserWelcome() {
const { user } = useContext(AppContext);
console.log('Re-rendering UserWelcome (expensive)...');
return <h1>Welcome, {user.name}!</h1>;
});
// ... in the App component ...
// <MemoizedUserWelcome />
This doesn’t work by itself! React.memo does a shallow comparison of props, but MemoizedUserWelcome has no props. It still re-renders because the context value it depends on changes. To fix this, you have to combine memo with extracting the component and passing state down as props. This shows the complexity and why splitting contexts is often better. A better memoization strategy involves splitting the component and memoizing the child.
A more advanced technique is to use useMemo to create a stable value for parts of the context, but this can get complicated quickly. Splitting contexts is almost always the cleaner, more scalable solution.
Practice Question
What is the primary performance drawback of using a single, large React Context for unrelated pieces of state?