We have all used context API when building software using ReactJS. One of the common problem of context API while using in large applications that when a context value changes, all components that consume that context triggers re-rendering, regardless of whether the components actually uses the specific part of the context that changed. This leads to performance issues for mid to large sized applications with complex component trees.
Why this happens? React doesn't automatically know or track which specific part or properties (like states) a component uses. React only knows that a component uses some of parts or properties of the context.
So because of this reason and because of this type of system design in react's context api, react notifies all consumers or we can say all components that consumes those context properties when a context value changes. For this reason, every consumers or components get re-rendered.
Basically in more simple terms when a context value gets changed, below things happen:
- React schedules a re-trigger or re-render for all the components that
- uses the useContext(Context) hook
- Are wrapped in
- Are descendants of a component that uses the context
First, we will demonstrate this problem via code. Use Vite to generate a react application, and put the below codes in the App.jsx file.
import { createContext, useContext, useState } from "react";
const AppContext = createContext();
function App() {
const [state, setState] = useState({
theme: { color: "blue", fontSize: 16 },
user: { name: "John", id: 1 },
settings: { notifications: true, language: "en" },
});
const changeTheme = () => {
setState((prev) => ({
...prev,
theme: {
...prev.theme,
color: prev.theme.color === "red" ? "blue" : "red",
},
}));
};
return (
<AppContext.Provider value={state}>
<div>
<button onClick={changeTheme}>Change theme</button>
{/* All these components will re-render when any part of context changes */}
<ThemeDisplay />
<UserProfile />
<SettingsPanel />
</div>
</AppContext.Provider>
);
}
function ThemeDisplay() {
const state = useContext(AppContext);
console.log("ThemeDisplay re-rendered");
return (
<div style={{ color: state.theme.color }}>Theme: {state.theme.color}</div>
);
}
function UserProfile() {
const state = useContext(AppContext);
console.log("UserProfile re-rendered");
return <div>User: {state.user.name}</div>;
}
function SettingsPanel() {
const state = useContext(AppContext);
console.log("SettingsPanel re-rendered");
return (
<div>Notifications: {state.settings.notifications ? "On" : "Off"}</div>
);
}
export default App;
First, inject the upper code into App.jsx and try to analyze and understand it properly. The code shows how React's context API causes all the consumers or the components using useContext to re-render whenever the context value changes, even if only part of the state is updated. The App
component manages a global state object containing theme
, user
and settings
and provides it to all children (ThemeDisplay
, UserProfile
and SettingsPanel
) through the AppContext.Provider
. A button exists in the App
component which toggles the theme.color
, which updates only the theme property in state. The childrens (ThemeDisplay
, UserProfile
and SettingsPanel
) using the same AppContext through useContext. All of these children also contains a console.log which prints a confirmation output of them being re-rendered.
Now after adding the code to your react app in App.jsx
, start the localhost server and open the react app in a window. Now open the Chrome developer tools
in the same window where the react app is running. Now try clicking the change theme button.
After that, open the console tab and there you will see all the children components displaying console log output text that was written in them if the Change theme
button is clicked. This indicates all of the children components being re-rendered even if only theme has changed.
To optimize this and reduce the re-rendering, one of the way is to turn the children components into standalone components and we need to pass only the relevant context data as props. We also need to wrap the children using memo from react. By this way, whenever the context updates the children won't re-render as long as the props used them haven't changed.
Now notice, analyze and try to understand the below code which follows this approach.
import { createContext, useState, memo } from "react";
const AppContext = createContext();
function App() {
const [state, setState] = useState({
theme: { color: "blue", fontSize: 16 },
user: { name: "John", id: 1 },
settings: { notifications: true, language: "en" },
});
const changeTheme = () => {
setState((prev) => ({
...prev,
theme: {
...prev.theme,
color: prev.theme.color === "blue" ? "red" : "blue",
},
}));
};
const changeUser = () => {
setState((prev) => ({
...prev,
user: { ...prev.user, name: prev.user.name === "John" ? "Jane" : "John" },
}));
};
const toggleNotifications = () => {
setState((prev) => ({
...prev,
settings: {
...prev.settings,
notifications: !prev.settings.notifications,
},
}));
};
return (
<AppContext.Provider value={state}>
<div>
<button onClick={changeTheme}>Change Theme</button>
<button onClick={changeUser}>Change User</button>
<button onClick={toggleNotifications}>Toggle Notifications</button>
<ThemeContainer theme={state.theme} />
<UserContainer user={state.user} />
<SettingsContainer settings={state.settings} />
</div>
</AppContext.Provider>
);
}
const ThemeContainer = memo(({ theme }) => {
console.log("ThemeContainer re-rendered");
return <ThemeDisplay color={theme.color} fontSize={theme.fontSize} />;
});
const UserContainer = memo(({ user }) => {
console.log("UserContainer re-rendered");
return <UserProfile name={user.name} id={user.id} />;
});
const SettingsContainer = memo(({ settings }) => {
console.log("SettingsContainer re-rendered");
return (
<SettingsPanel
notifications={settings.notifications}
language={settings.language}
/>
);
});
const ThemeDisplay = memo(({ color, fontSize }) => {
console.log("ThemeDisplay re-rendered");
return (
<div style={{ color, fontSize: `${fontSize}px` }}>
Theme: {color}, Font Size: {fontSize}px
</div>
);
});
const UserProfile = memo(({ name, id }) => {
console.log("UserProfile re-rendered");
return (
<div>
User: {name} (ID: {id})
</div>
);
});
const SettingsPanel = memo(({ notifications, language }) => {
console.log("SettingsPanel re-rendered");
return (
<div>
Notifications: {notifications ? "On" : "Off"}, Language: {language}
</div>
);
});
export default App;
Now, let's analyze how the upper code snippet implements the idea of passing standalone components and wrapping the children components using memo. First of all, replace the previous App.jsx
code file to upper code snippet.
Now start the react localhost server and open it in a window and in the same window open the chrome developer tools. Now click any of the Change Theme
, Change User
, Toggle Notifications
button and we will see that only relevant children component logged the output, which eventually indicates that only that specific children component got re-rendered and other children components didn't get re-rendered.
So, how our approach solved the re-rendering issue? First, we extracted the children as standalone components, we have ThemeContainer
, UserContainer
, SettingsContainer
as children components. After that, instead of them using the entire AppContext
using useContext
, App
passes only the relevant slice of states for example here the theme
, user
or settings
.
If we had passed the entire state to every child components, then even when only one property for example the theme color changes, react would have treated the state object as new
. This means all the children components would receive a new
prop reference and they would get re-rendered, even if the user
or settings
part of the state hadn't changed.
By extracting the standalone components and by passing them only the exact piece of state they actually need, react now can do a much better and finer comparison. For example, here the ThemeContainer
only gets the theme
object, so changing the user
or settings
doesn't affect it all. And also notice that to avoid un-necessary re-renders, we have also used memo to wrap the children and because of the usage of this memo, a children component will not re-render if it's props haven't change, even though the parent App
re-renders when the overall state changes.
So, the final outcome? Now changing the theme will not trigger UserProfile
/ SettingsPanel
to re-render. And changing the user will not trigger ThemeDisplay
or SettingsPanel
to re-render. So overall, only the relevant components get re-rendered, exactly as we intended to optimize the re-rendering issue.
Top comments (0)