We can't overstate how much of a key role performance plays when it comes to user experience.
Nothing can make a user leave your app quicker than a sluggish or laggy user interface (UI), and often times this results from poor coding practices on the part of the developer.
A lot of React developers use the context API in a way that results in pointless UI rerenders and ultimately a slow application. While its effect might be subtle in smaller apps, it becomes quite noticeable in large applications.
So what is this bug we’re going to be talking about? Read on to find out!
Sidenote: If you’re new to learning web development, and you’re looking for the best resource to help with that, I strongly recommend HTML to React: The Ultimate Guide.
The Problem of Rerendering The Whole Application
Consider the following App component, which returns a main element housing two custom components: ExampleComponent1 and ExampleComponent2. Inside ExampleComponent1, we’re keeping track of the count state:
import {useState} from "react"
export default function App() {
return (
<main>
<ExampleComponent1 />
<ExampleComponent2 />
</main>
)
}
export function ExampleComponent1 {
const [count, setCount] = useState(0)
return <div>Example component 1</div>
}
export function ExampleComponent2 {
return <div>Example component 2</div>
}
Now let’s say we later on discover that ExampleComponent2 will also need access to the count state.
What you typically do is lift the state up to the parent component. But oftentimes, in real-world scenarios, a lot of other components might need access to the same state.
Rather than manually passing down the props into the various components, the better approach would be to use the context API. Let’s bring it into our App component:
import {useState, useContext} from "react"
const CountContext = useContext(null)
export default function App() {
const [count, setCount] = useState(0)
return (
<main>
<CountContext.Provider value={{count, setCount}}>
<ExampleComponent1 />
<ExampleComponent2 />
</CountContext.Provider>
</main>
)
}
export function ExampleComponent1 {
const [count, setCount] = useContext(CountContext)
return <div>Example component 1</div>
}
export function ExampleComponent2 {
return <div>Example component 2</div>
}
Basically, you’re to wrap the part of your app that needs access to the global state in the Context Provider component.
You’d then pass the variables you want the children components to access, which, in our case, is the count variable and setCount() method.
Now here comes the mistake.
Keep in mind that we’re consuming the context in ExampleComponent1, but not in ExampleComponent2. The idea is that, when we change the state in the parent App component, it’s only the components using the context that get re-rendered.
But the reality is that both components nested in the provider will be rerendered when the state changes.
To demonstrate this, let’s log different messages from both components and add a button in ExampleComponent1 that, when clicked, updates the state:
// App component goes here
export function ExampleComponent1 {
const [count, setCount] = useContext(CountContext)
console.log("ExampleComponent1 rendering")
return (
<div>Example component 1
<button onClick={() => setCount(count + 1)}> Click me</button>
</div>
)
}
export function ExampleComponent2 {
console.log("ExampleComponent2 rendering")
return <div>Example component 2</div>
}
Because only the first component is using the context, the second should not be affected when we click the button to update the state, right?
Wrong!
When you open your browser’s console, refresh the page, and click the button, you’ll see the messages in both components being logged there. This means that both ExampleComponent1 and ExampleComponent2 have been re-rendered.
Now imagine that you have the provider high up in your component tree. The whole app will likely rerender every time you make a change. This is very inefficient and could result in significantly lowered performance in large applications.
The Solution is to Put The React Context in a Separate File
The solution to this problem is quite straightforward.
You’d start by creating a new folder named contexts inside of the src/ directory within your project’s directory structure.
Then inside the src/contexts directory, create a file named count-contexts.jsx and paste in the following code:
import React from "react"
export const CountContext = useContext(null)
export default function CountContextProvider({children}) {
const [count, setCount] = useState(0)
return (
<CountContext.Provider value={{count, setCount}}>
{children}
</CountContext.Provider>
)
}
You’re basically moving the context provider, along with the global state, to a separate file. And this time, the provider component accepts the children from the parent.
Now in your App component, import the CountContextProvider and the CountContext variable from the newly created file, then use the former to wrap the children elements:
import CountContextProvider, {CountContext} from './contexts/count-context'
export default function App() {
const [count, setCount] = useState(0)
return (
<main>
<CountContextProvider>
<ExampleComponent1 />
<ExampleComponent2 />
</CountContextProvider>
</main>
)
}
// Other components
Now when you save the file, open your browser’s console, refresh the browser, and click the button, you’ll notice that only ExampleComponent1 is being re-rendered. And this is because it’s the only one using the context.
Conclusion
If you’re building a large application with a lot of components requiring the same state, then it’s crucial that you use the technique we covered in this article to improve your app’s performance, and consequently, the user’s experience.
Want to collaborate with me? Fill out this form.
Top comments (0)