Fixing Re-Renders When Using Context in React
Some months ago, I was refactoring a React project, and I was stuck in one problem for hours. The refactor was because of a common problem in React projects: Pass a lot of props to the child components, then you have to pass them to the child of them, and so. When this happens, if you want to reuse those components on another part of the app, you have to get information in your new component that maybe you don’t need to worry about that time.
Versión en español aquí
I separated the data into many contexts, so I only share the necessary data with the component that needs them. So I stopped to pass a lot of props in every component. Even that sounds like a successful refactor, it wasn’t. My components keep updating when I updated an state of a context which they didn’t depend on. It doesn’t make sense, right?
To explain my problem, I’ll give you an example. I’ll have 3 components:
SessionForm
: Component to add a username. If you have already entered it, it shows a greeting and a button to log out (delete the username). If you haven’t entered it, it shows you an entry to add it.SessionCounterMessage
: Component that shows a message with the username entered or aYou
and the number returned by a counter.CounterButtons
: Component with a counter and 2 buttons that allow you to add or subtract from the counter.
Based on my first solution, I would create 2 contexts. One for the username (SessionContext
) and one for the counter ( CounterContext
). Then the dependency of contexts of my components would look like this:
SessionForm
depends onSessionContext
CounterButtons
depends onCounterContext
SessionCounterMessage
depends onSessionContext
andCounterContext
This was my initial solution:
function App() {
const [currentUser, setCurrentUser] = React.useState(null);
const [counter, setCounter] = React.useState(1);
return (
<SessionContext.Provider
value={React.useMemo(() => ({ currentUser, setCurrentUser }), [
currentUser,
setCurrentUser,
])}
>
<CounterContext.Provider
value={React.useMemo(() => ({ counter, setCounter }), [
counter,
setCounter,
])}
>
<SessionForm />
<SessionCounterMessage />
<CounterButtons />
</CounterContext.Provider>
</SessionContext.Provider>
);
}
I added a console.log to my components to make you aware of my error, I added a console.log to my components so that they see how many times it was rendered:
There you can see, when I update the counter, it re-renders the SessionForm
component. Even when it doesn’t depend on the CounterContext
context, which has counter
state.
And when I update the username, it re-renders the CounterButtons
component. Even when it doesn’t depend on the SessionContext
context, which has username
as a state.
Now you see my code, do you find my mistake? Well, I didn’t find any mistakes in my code if I had separated them into different contexts. Why did they keep re-render all the components?
What I did was ask for help. I asked @sergiodxa, who has been using React longer, and he said: This
const MyContext = React.useContext({});
function App() {
const [state, setState] = React.useState(false);
return (
<MyContext.Provider value=>
<MyCustomComponent />
</MyContext.Provider>
);
}
is different from this:
const MyContext = React.useContext({});
function MyContextProvider({ children }) {
const [state, setState] = React.useState(false);
return (
<MyContext.Provider value=>
{children}
</MyContext.Provider>
);
}
function App() {
return (
<MyContextProvider>
<MyCustomComponent />
</MyContextProvider>
);
}
He didn’t explain why at that time; maybe he was busy, I don’t remember. But I realized that I was rendering my component in the same place that I created my states. Every time I updated the state, it re-rendered my parent component, which re-render all its children.
With this in my mind, I’ll change my initial example to check it works.
function SessionProvider({ children }) {
const [currentUser, setCurrentUser] = React.useState(null);
return (
<SessionContext.Provider
value={React.useMemo(() => ({ currentUser, setCurrentUser }), [
currentUser,
setCurrentUser,
])}
>
{children}
</SessionContext.Provider>
);
}
function CounterProvider({ children }) {
const [counter, setCounter] = React.useState(1);
return (
<CounterContext.Provider
value={React.useMemo(() => ({ counter, setCounter }), [
counter,
setCounter,
])}
>
{children}
</CounterContext.Provider>
);
}
function App() {
return (
<SessionProvider>
<CounterProvider>
<SessionForm />
<SessionCounterMessage />
<CounterButtons />
</CounterProvider>
</SessionProvider>
);
}
Here you can see the logs when every component is rendered
It works! No more unnecessary renders!
It could look like a small change, and even you could think the user won’t notice this change. But the components I was refactoring rendered audios and videos. Every time I updated the audios, the videos would be re-rendered, and it looks like a bug in the app.
If you made it this far, thanks for reading. ❤️