React context, performance?
Romain Trotard
Posted on November 10, 2021
Today we are going to talk about React context. Its role is sometimes mistaken, badly said as a mini-redux. Firstly we are going to see what it is, then talk about performance and workarounds we have.
What is it?
I can't have a better definition than in the documentation:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Conceptually, you will put data in a React context and provides it to a React sub-tree component thanks to a Provider. Then in all components in this sub-tree, you are able to get the data thanks to a Consumer. At every changes of the data in the context, each consumers will be notified.
So there is no notion of state management here, so don't be confused, React context is not a mini-redux. But you can simulate it, if you combine it with state
or reducer
. However, you have to be aware that redux supplies some features like:
- time travelling
- middlewares
- performance optimizations
How to use React context
Creation of a context
The creation is made thanks to the createContext
method pulls from React
. This method takes as only parameter the default value that is optional:
const MyContext = React.createContext();
Provider
The Provider
is accessible through the created context:
const MyProvider = MyContext.Provider;
The Provider
obtained is a Component has the following prop:
- a value: the value you want to provide to children components
- children: the children to which you want to provide the value
<MyProvider value={valueToProvide}>
{children}
</MyProvider>
Note: Make a
Provider
component that takeschildren
instead of putting directly components in the Provider like this:
function App() {
const [data, setData] = useState(null);
return (
<MyContext.Provider value={{ data, setData }}>
<Panel>
<Title />
<Content />
</Panel>
</MyContext.Provider>
);
}
Doing like this each time the setData
is called, it will render all components Title
, Content
and Panel
even if they do not use the data
.
So instead do:
function MyProvider({ children }) {
const [data, setData] = useState(null);
return (
<MyContext.Provider value={{ data, setData }}>
{children}
</MyContext.Provider>
);
}
function App() {
return (
<MyProvider>
<Panel>
<Title />
<Content />
</Panel>
</MyProvider>
);
}
Consumer
Once we provide some data, we probably want to get it somewhere in a child. There is 2 ways to get it:
- with
useContext
hook - with the
Consumer
component provided by the context we created
useContext
It's the hook to consume value from the context. You just have to pass the context to the hook:
const myValue = useContext(MyContext);
Note: You may want to create a custom hook
useMyContext
not to export the context directly. It will allow you to make some check to ensure that theProvider
has well been added:
const useMyContext = () => {
const value = useContext(MyContext);
if (!value) {
throw new Error(
"You have to add the Provider to make it work"
);
}
return value;
};
Warning: The check will not work if you put a default value in the context.
Consumer
component
As said previously, the created context exports a Consumer
component too (like Provider
), you can then get the value by passing a function as children:
<MyContext.Consumer>
{(value) => {
// Render stuff
}
</MyContext.Consumer>
Recommendation and property
Put context the closest to where it's used
An advice is to put Provider
s the closest to where it's being used. I mean do not put all your Provider
s at the top of your app. It will help you to dive in the codebase, with separation of concern and should help React to be slightly faster because would not have to cross all the tree components.
Note: You will need to put some
Provider
at the top of your application. For example:I18nProvider
,SettingsProvider
,UserProvider
, ...
Doing this, you will may encounter some performance issues when parent re-render if you pass an object as value (most of the time it will be the case).
For example if you have:
const MyContext = React.createContext();
function MyProvider({ children }) {
const [data, setData] = useState(null);
const onClick = (e) => {
// Whatever process
};
return (
<MyContext.Provider value={{ data, onClick }}>
{children}
</MyContext.Provider>
);
}
function ComponentUsingContext() {
const { onClick } = useContext(MyContext);
return <button onClick={onClick}>Click me</button>;
}
const MemoizedComponent = React.memo(ComponentUsingContext);
function App() {
const [counter, setCount] = useState(0);
return (
<div>
<button
onClick={() => setCounter((prev) => prev + 1)}
>
Increment counter: counter
</button>
<MyProvider>
<MemoizedComponent />
</MyProvider>
</div>
);
}
In this case, when we increment the counter, MemoizedComponent
will re-render even it's memoized because the value in the context changes.
In this case the solution is to memoized the value:
const value = useMemo(() => {
const onClick = (e) => {
// Whatever process
};
return {
data,
onClick,
};
}, [data]);
And tada, MemoizedComponent
do not render anymore when incrementing the counter.
Warning: Memoizing the value is only useful if the components using the context are memoized. Otherwise it will just do nothing and continue re-rendering because parent re-renders (in the previous case the parent is App).
Nested providers
It's possible to do nested Provider for same context. It's for example used in the react-router
implementation, see my article.
In this case, Consumers will get the value of the closest Provider to them.
const MyContext = React.createContext();
export default function App() {
return (
<MyContext.Provider value="parent">
<ParentSubscriber />
<MyContext.Provider value="nested">
<NestedSubscriber />
</MyContext.Provider>
</MyContext.Provider>
);
}
function ParentSubscriber() {
const value = useContext(MyContext);
return <p>The value in ParentSubscriber is: {value}</p>;
}
function NestedSubscriber() {
const value = useContext(MyContext);
return <p>The value in NestedSubscriber is: {value}</p>;
}
In the previous example, ParentSubscriber
will get the value parent
and in the other side NestedSubscriber
will get nested
.
Performance
To talk about performance we are going to do a little music app with few features:
- be able to see what's our friends are listening
- show musics
- show the current music
Important: Do not judge the use of the React context features. I know that for such features we would probably not use React context in real world.
Friends and musics features
Specifications:
- friends feature consists to fetch every 2sec a fake API that will return an array of object of this type:
type Friend = {
username: string;
currentMusic: string;
}
- musics feature will fetch only once the available music and will return:
type Music = {
uuid: string; // A unique id
artist: string;
songName: string;
year: number;
}
Okay. Let's implement this.
Innocently, I want to put all this data in a same context and provide it to my application.
Let's implement the Context and Provider:
import React, {
useContext,
useEffect,
useState,
} from "react";
const AppContext = React.createContext();
// Simulate a call to a musics API with 300ms "lag"
function fetchMusics() {
return new Promise((resolve) =>
setTimeout(
() =>
resolve([
{
uuid: "13dbdc18-1599-4a4d-b802-5128460a4aab",
artist: "Justin Timberlake",
songName: "Cry me a river",
year: 2002,
},
]),
300
)
);
}
// Simulate a call to a friends API with 300ms "lag"
function fetchFriends() {
return new Promise((resolve) =>
setTimeout(() => {
resolve([
{
username: "Rainbow",
currentMusic:
"Justin Timberlake - Cry me a river",
},
]);
}, 300)
);
}
export const useAppContext = () => useContext(AppContext);
export default function AppProvider({ children }) {
const [friends, setFriends] = useState([]);
const [musics, setMusics] = useState([]);
useEffect(() => {
fetchMusics().then(setMusics);
}, []);
useEffect(() => {
// Let's poll friends every 2sec
const intervalId = setInterval(
() => fetchFriends().then(setFriends),
2000
);
return () => clearInterval(intervalId);
}, []);
return (
<AppContext.Provider value={{ friends, musics }}>
{children}
</AppContext.Provider>
);
}
Now let's see the implementations of the Friends
and Musics
component. Nothing complicated:
function Friends() {
const { friends } = useAppContext();
console.log("Render Friends");
return (
<div>
<h1>Friends</h1>
<ul>
{friends.map(({ username, currentMusic }) => (
<li key={username}>
{username} listening {currentMusic}
</li>
))}
</ul>
</div>
);
}
And:
function Musics() {
const { musics } = useAppContext();
console.log("Render Musics");
return (
<div>
<h1>My musics</h1>
<ul>
{musics.map(({ uuid, artist, songName, year }) => (
<li key={uuid}>
{artist} - {songName} ({year})
</li>
))}
</ul>
</div>
);
}
Now, I will ask you a question. Do you know what will be rendered / print in the console?
Yep, both Friends
and Musics
will rendered every around 2sec. Why?
Do you remember that I told you each consumers will be trigger if the value provided changes, even if they use a part of this value that does not change.
It's the case of Musics
that only pulls musics
, that does not change, from the context.
You can see it in the following codesandbox:
It's why I advise to separate data by business domain in different contexts.
In our example I will make two separate contexts FriendsContext
and MusicContext
.
You can see the implementation here:
Current listening music
Now we would like to be able to select a music from the list, and listen it.
I'm going to do a new context to store the currentMusic
:
import React, { useContext, useState } from "react";
const CurrentMusicContext = React.createContext();
export const useCurrentMusicContext = () =>
useContext(CurrentMusicContext);
export default function CurrentMusicProvider({ children }) {
const [currentMusic, setCurrentMusic] =
useState(undefined);
return (
<CurrentMusicContext.Provider
value={{ currentMusic, setCurrentMusic }}
>
{children}
</CurrentMusicContext.Provider>
);
}
I add a button in the Musics
component to listen the associated music:
function MyMusics() {
const musics = useMusicContext();
const { setCurrentMusic } = useCurrentMusicContext();
console.log("Render MyMusics");
return (
<div>
<h1>My musics</h1>
<ul>
{musics.map((music) => (
<li key={music.uuid}>
{getFormattedSong(music)}{" "}
<button onClick={() => setCurrentMusic(music)}>
Listen
</button>
</li>
))}
</ul>
</div>
);
}
And the CurrentMusic
component is simply:
function CurrentMusic() {
const { currentMusic } = useMusicContext();
console.log("Render CurrentMusic");
return (
<div>
<h1>Currently listening</h1>
{currentMusic ? (
<strong>{getFormattedSong(currentMusic)}</strong>
) : (
"You're not listening a music"
)}
</div>
);
}
Ok, now what is happening when you chose to listen a new music?
Currently, both MyMusics
and CurrentMusic
will render. Because when the currentMusic
changes a new object is passed to the provider.
Separate dynamic
and static
data
One strategy is to separate dynamic and static data in two different contexts CurrentMusicDynamicContext
and CurrentMusicStaticContext
:
import React, { useContext, useState } from "react";
const CurrentMusicStaticContext = React.createContext();
const CurrentMusicDynamicContext = React.createContext();
export const useCurrentMusicStaticContext = () =>
useContext(CurrentMusicStaticContext);
export const useCurrentMusicDynamicContext = () =>
useContext(CurrentMusicDynamicContext);
export default function CurrentMusicProvider({ children }) {
const [currentMusic, setCurrentMusic] =
useState(undefined);
return (
<CurrentMusicDynamicContext.Provider
value={currentMusic}
>
<CurrentMusicStaticContext.Provider
value={setCurrentMusic}
>
{children}
</CurrentMusicStaticContext.Provider>
</CurrentMusicDynamicContext.Provider>
);
}
And here we go. Just to use the right hook to get value from the context.
Note: As you can see it really complicates the code, that's why do not try to fix performance issue that does not exist.
use-context-selector
The second solution is to use the library made by dai-shi named use-context-selector
. I made an article about its implementation.
It will wrap the native context API of React, to give you access to multiple hooks that will re-render your component only if the selected value from the store changed.
The principle is simple, you create your context thanks to the createContext
function given by the lib.
Then you select data from it with the useContextSelector
. The API is:
useContextSelector(CreatedContext, valueSelectorFunction)
For example if I want to get the currentMusic
:
const currentMusic = useContextSelector(
CurrentMusicContext,
(v) => v.currentMusic
);
Not to expose the context I made a hook:
export const useCurrentMusicContext = (selector) =>
useContextSelector(CurrentMusicContext, selector);
And that's all. You can find the code bellow:
Conclusion
We have seen how to use React context, and performance issues that you can encounter.
But like always, do not do premature optimization. Just try to worry about it when there are real problems.
As you have seen, optimization can make your code less readable and more verbose.
Just try to separate different business logics in different context and to put your provider as close as possible to where it's needed, to make things clearer. Do not put everything at the top of your app.
If you have real performance problems because of contexts, you can:
- separate dynamic and static data in different contexts
-
useMemo
the value if it's changing because of parent re-rendering. But you will have to put somememo
on components that uses the context (or parent) otherwise it will do nothing. - use the
use-context-selector
lib to solve context's shortcomings. Maybe one day natively inreact
as you can see in this opened PR. - one other strategy that we don't talk about it in this article, is not to use React context but atom state management library like:
jotai
,recoil
, ...
Do not hesitate to comment and if you want to see more, you can follow me on Twitter or go to my Website.
Posted on November 10, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.