2 re-rendering pitfalls I learned about while building my React app
Tags: react
Published: 2024-06-15 14:45:31

Hi It's Hudy here.

In this blog post, I'd like to share two re-rendering pitfalls I encountered while developing [Namviek](https://namviek.com), 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](https://stackoverflow.com/a/56346042/17752111)

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.