No, react context is not causing too many renders
I commonly see people having the belief that react context is not appropriate for managing state, because every time its state changes, it'll cause everything under the React provider to re-render.
This is causes people to avoid using context, and jumping straight to tools like Redux or Zustand.
It is a misnomer, and I'm here to disprove it.
Here's my application:
Top level of the application
26export function ReactRenders1() {
27 const [value, setValue] = React.useState("foo");
28
29 return <MyProvider>
30
31 <button onClick={() => {
32 setValue(`${Math.random()}`);
33 }}
34 className="global-render-button"
35 > Render all</button>
36
37 <div className="render-tracker-demo">
38
39 <StateChanger />
40 <StateDisplayer />
41
42 <SomeUnrelatedComponent />
43 <SomeUnrelatedComponent />
44 <SomeUnrelatedComponent />
45 </div>
46 </MyProvider >
47}
48
I do have a button at the top of the application that will re-render the whole application.
This is to demonstrate that there is no trickery here.
Context provider
18
19const MyProvider = ({ children }: { children: React.ReactNode }) => {
20 const [value, setValue] = React.useState("foo");
21 const contextValue: MyContextType = { value, setValue };
22 return <MyContext.Provider value={contextValue}>{children}</MyContext.Provider>;
23};
My context provider is simple. We store state in a useState
hook, and provide it via the context provider.
Context consumers
51function StateChanger() {
52 const { setValue } = useContext(MyContext);
53 return <div className="state-changer">
54 <strong>State Changer</strong>
55
56 <button onClick={() => setValue(`${Math.random()}`)}>Change state</button>
57 <RenderTracker />
58 </div >
59}
60
61function StateDisplayer() {
62 const { value } = useContext(MyContext);
63 return <div className="state-displayer">
64 <strong>State Displayer</strong>
65 <div>{value}</div>
66 <RenderTracker />
67 </div>
68}
I have two components, both of them use this context.
Unrelated component
70function SomeUnrelatedComponent() {
71 return <div className="some-unrelated-component">
72 <strong>Some unrelated component</strong>
73 <RenderTracker />
74 </div>
75}
I have several instances of an unrelated component that doesn't use the context.
Render tracking component
Lines 2 to 13 in 5f046fb
2export function RenderTracker() {
3
4 let randX = Math.floor(Math.random() * 100);
5 let randY = Math.floor(Math.random() * 100);
6
7
8 return <div className="render-tracker">
9 <strong>Render Tracker</strong>
10 <div className="render-tracking-dot" style={{ top: `${randY}%`, left: `${randX}%` }}>
11 </div>
12 </div >
13}
My render tracking component displays the dot in a different spot each time it renders.
What's the result?
You can see for yourself:
☝️ Interactive demo
Observe that clicking 'render all' button will in fact cause a render of the entire application.
Observe that clicking the 'Change state' button, only affect the components consuming the context.
Where does this confusion come from?
I think this confusion comes from two things.
1. You really shouldn't bung all your state into one provider.
Were I to add color/setColor, foo/setFoo and bar/setBar pairs to the same context provider, and had a new component FooComponent, using those new parts of state, these state changes will cause re-renders. All the consumers of the one context provider will re-render when state changes.
☝️ Interactive demo
Code
83function FooComponent() {
84 const { color, setColor } = useContext(MyContext);
85 return <div className="foo-component">
86 <strong>Foo Component</strong>
87 <button onClick={() => {
88 // This is Copilots suggestion lol
89 const randomColor = `#${Math.floor(Math.random() * 16777215).toString(16)}`;
90 setColor(randomColor);
91 }}>Randomize color</button>
92 <div className="color-display" style={{ backgroundColor: color }}></div>
93 <RenderTracker />
94 </div>
95}
Observe that randomizing the color causes renders of the other context consumers.
This is fine if it's all related data and they needed to show the change anyway.
But if you have two sets of unrelated data, then you can just use two context providers!
{children} don't cause renders
I think a lot of the confusion comes from knowing that the render of a component will cause all of its descendants to render.
And because context providers usually live at the top of the application, people believe that the context provider, when it re-renders, will cause everything below it to render.
Unfortunately, the terminology is a bit confusing here!
Consider two seemingly similar components
8export function ChildrenStyleOne() {
9 const [value, setValue] = React.useState(0)
10 return <div className="some-parent-component">
11 <button onClick={() => {
12 setValue((prev) => prev + 1);;
13 }}>Increase count: {value}</button>
14 {/* ð Here we declare the RenderTracker directly in the component */}
15 <RenderTracker />
16 </div >
17}
18
19export function ChildrenStyleTwo(props: React.PropsWithChildren) {
20 const [value, setValue] = React.useState(0)
21 return <div className="some-parent-component">
22 <button onClick={() => {
23 setValue((prev) => prev + 1);;
24 }}>Increase count: {value}</button>
25 {/* ð Here, it is passed from the parent via the `children` prop */}
26 {props.children}
27 </div >
28}
The first directly renders the RenderTracker.
The second has it passed in via the children
prop.
The terminology is a bit ambiguous, in both cases these can be called 'children' in common parlance.
However, they behave a lot differently!
☝️ Interactive demo
Observe that the RenderTracker that is passed in as a child does not re-render when the state changes.
Conclusions
React context is not the performance boogeyman that it often made out to be.
This common misconception has people reaching for tools like Redux and Zustand when it's really not needed.
Yes, if you load up dozens of bits of state into one context provider, then you're going to have problems.
But to just pass state between components that are in different parts of your application, it's absolutely fine, and dare I say - it's much tidier solution using a global state provider like Redux of Zustand.
If you really want a performance boogeyman, it's controlled components.
For example here - we can see every keystroke causes a render:
☝️ Interactive demo
Code
9export function ReactRenders4() {
10 const [value, setValue] = useState('')
11 return <div className="render-tracker-demo">
12 <div className="some-parent-component">
13 <input type="text" value={value} onChange={(e) => setValue(e.target.value)} placeholder="type here" />
14 <RenderTracker />
15 </div>
16 </div >
17}
Type in the text box and note a render on every keystroke.
Don't be afraid of context providers. It's often the perfect tool for the job.
Questions? Comments? Criticisms? Get in the comments! 👇
Spotted an error? Edit this page with Github