Hi It's Hudy here.
In this blog post, I'd like to share two re-rendering pitfalls I encountered while developing Namviek, my open-source project management app, and how I addressed them.
These pitfalls highlight common mistakes that can lead to performance issues in React applications. By understanding and avoiding these issues, you can ensure that your React apps run smoothly and efficiently.
1. Put states, Context.Provider and child component within the same place.
I usually use React Context for building my own components (Ex: Calendar, ProjectMemberSelect) and containers. However, by habit, I tend to put states, Context.Provider and all child components within the same place.
Look at the below example: I put the counter
state and <Report.Provider/>
inside <Report/>
. No problems, I thought initially.
export default function Report() {
const [counter, setCounter] = useState(1) // state
return (
<Report.Provider value={{
counter,
setCounter
}}>
<ReportContent /> // child
<ReportSidebar /> // child
</Report.Provider>
)
}
The issue just appeared when I increased counter
value. Both <ReportContent />
and <ReportSidebar/>
re-rendered, even though they didn't rely on the counter
state.
This might seem like a minor oversight, but it's a common pitfalls for developers who frequently use React Context like me, I guess.
// Fixed version
export default function Report() {
// moved state to another place
return (
<ReportProvider>
<ReportContent />
<ReportSidebar />
</ReportProvider>
)
}
function ReportProvider({children}) {
const [counter, setCounter] = useState(1)
return <Report.Provider value={{
counter,
setCounter
}}>{children}</Report.Provider>
}
So the solution is straightforward: move the counter
state logic into the <ReportProvider/>
component. By doing this, only the components that truly depend on the context will re-render when the state changes.
This fix works because the counter
state becomes encapsulated within the <ReportProvider/>
component. Since child components only receive the context value at the time of render, changes to the state within the provider won't trigger re-renders in <ReportContent/>
and <ReportSidebar/>
unless they explicitly rely on the context.
Crucially, the children
prop, which passes down the component tree, only updates when the <Report />
component itself re-renders.
2. Misconception: Custom hooks do not cause component re-renders.
Look the following example. I created a custom hook called useUpdate
to encapsulate the fetch logic with the goal of minimizing unnecessary re-renders.
// custom hook
function useUpdate() {
const [counter, setCounter] = useState(0)
useEffect(() => {
// assume that this is a fetch call
setTimeout(() => {
setCounter(c => c + 1)
}, 1000)
}, [])
}
// component
export default function Report() {
useUpdate()
console.log('render Report'). // run twice
return (
<ReportProvider>
<ReportContent />
<ReportSidebar />
</ReportProvider>
)
}
And I realized the <Report/>
component was still re-rendering as expected. While I thought custom hooks might prevent re-renders.
After a few hours of researching I found that.
A custom can be treated as a function simply which is executed from within the functional component and effectively the hooks that are present in the custom hook are transferred on to the component. So any change that would normally cause the component to re-render if the code within the custom hook was directly written within functional component will cause a re-render even if the hook is a custom hook. Refer
So to fix this, I moved the useUpdate
hook to a separate component like <PrefetchData/>
.
// fixed version
function PrefetchData() {
useUpdate()
return null
}
// component
export default function Report() {
console.log('render Report'). // run once
return (
<ReportProvider>
<PrefetchData />
<ReportContent />
<ReportSidebar />
</ReportProvider>
)
}
Now, the <PrefetchData/>
component will be responsible for retrieving the data and managing it's state. As a result, the re-render process will be isolated to the <PrefetchData/>
component, preventing unintended re-renders in unrelated components outside its scope.
This approach ensures that updates to the report's state only trigger re-renders in components that rely on the fetched data, promoting a more efficient rendering cycle.
Conclusion
I hope my experience can help others prevent performance issues caused by unintentional misuse of React.Context. If you have any insights into better solutions or have spotted any mistakes in my explanation, I'd be eager to learn from them.
Top comments (0)