Filipe Colaquecez

Filipe Colaquecez

The Pitfalls of Creating Components Inside the Body Function of a React Component

javascript, react, best practices, react-native, antipattern

How creating a component inside the component function definitions can cause problems in app development, and how to avoid it by using an external or memoized component.

In this post, I'm going to show you a common antipattern I've been seeing a lot: defining components inside a React component's body. Even though it might seem like a quick and easy solution, it can cause a bunch of problems during the app development process.

When we create new components, it's common for them to gradually become huge. To deal with this, we often isolate that code in global folders so it can be reused in other places. However, there are cases where this code is only used within a specific context, as in this example:

const Profile = () => {
  return (
    <>
      <h1>My Profile</h1>
      <h1>Notifications</h1>
      <p>10</p>
    </>
  )
}
export default Profile

In that scenario, we could create a new component called Notification, and with the idea of making the code easier to read and organized, many devs would do this:

const Profile = () => {
  const Notification = () => {
    return (
      <>
        <h1>Notifications</h1>
        <p>10</p>
      </>
    )
  }
  return (
    <>
      <h1>My Profile</h1>
      <Notification />
    </>
  )
}
export default Profile

The problem

When the Profile re-renders, it will pass a new reference to the Notification, which will create a new instance of the component. React will then compare the current UI Tree with the previous one, and since they will be different, the component will be unmounted and then mounted again.

book icon
DEEP DIVE

UI Tree? Same position, same state?

React uses JSX/TSX code to create the UI Tree. It is then used by React to generate the Virtual DOM.

When changes are made, React will compare the new changes with the previous state of the UI Tree to generate a new version of it.

This updated UI Tree is then used to generate a new version of the Virtual DOM.

Here's an example to help illustrate this concept:

Try increasing the value of both examples and then clicking the toggle button. You'll notice that the example Parent with different position and type resets its state.

This happens because we are altering the position of the UI Tree.

show ? (
  <Component title="Component 1" />
) : (
  <div>
    <Component title="Component 2" />
  </div>
)

However, if we do not change the UI Tree position

show ? <Component title="Component 1" /> : <Component title="Component 2" />

The Component will not remount since it is the same component in the same position, and from React's perspective, it remains unchanged.

That's why when we create a component in the body function and pass it to the render function, the component will unmount and then mount again. This occurs because a new component is generated each time the parent component re-renders, resulting in a new UI Tree.

In React, the state does not live inside the component itself. React holds that state and links it to the relevant components based on the UI Tree position. 🤯

This is a bad practice because Notification could just be re-rendered, but instead, it is recreated from scratch whenever the Profile re-render, wasting more resources and potentially causing side-effects in the code.

Side Effects:

  • useEffect ignoring the dependency array
  • Flicker
  • Reference lost
  • States e props reseted

Try incrementing the count and then press the Random re-render button

Solutions

Extern Component

In my opinion, the best solution for this scenario would be to create a components folder inside the Profile folder, This will make the code structure more organized and scalable. If the Profile grows and has several sub-components, they will all be in the same place, and you wouldn't have the problem with re-mounting.

so we would have something like this:

screens
--Profile
---components
----Notification
-----Notification.tsx
-----Notification.styles.ts
---Profile.tsx
---Profile.styles.ts
---Profile.test.ts

In addition, you may consider moving the component outside the body function of Profile, like this:

const Notification = () => {
  return (
    <>
      <h1>Notifications</h1>
      <p>10</p>
    </>
  )
}

const Profile = () => {
  return (
    <>
      <h1>My Profile</h1>
      <Notification />
    </>
  )
}
export default Profile

With the changes made, the Notification reference will remain consistent even when the Profile re-render. Therefore, the Notification will not be unmounted and then mounted.

Helper Function - React Element

We can create a function that returns a React Element, and React will update only what's necessary.

If you have worked with FlatList in React Native, you may have used the renderItem prop. It's a good practice to create the function passed to this prop inside the component body function.

The documentation contains additional information on this topic

import { useState } from "react"

export default function App() {
  const [count, setCount] = useState(0)

  const notification = ({ title }) => {
    return <h1>{title}</h1>
  }

  return (
    <div>
      {notification({ title: count })}
      <button onClick={() => setCount(state => state + 1)}>Increment</button>
    </div>
  )
}

Memoized Component

By utilizing the useMemo hook, we can prevent the Notification component from being unmounted and remounted. This hook maintains the same reference of the component between re-renders, avoiding any remounting issues.

See it in codesandbox and open the console :)

The code:

import React, { useEffect, useMemo, useState } from "react"
const Profile = () => {
  const [notificationCount, setNotificationCount] = useState(0)

  const Notification = ({ count }) => {
    console.log("re-render")
    useEffect(() => {
      console.log("mounted")
      return () => console.log("unmounted")
    }, [])
    return (
      <>
        <h1>Notifications</h1>
        <p>{count}</p>
      </>
    )
  }
  const NotificationMemo = useMemo(() => Notification, [])
  return (
    <>
      <h1>My Profile</h1>
      <NotificationMemo count={notificationCount} />
      <button onClick={() => setNotificationCount(state => state + 1)}>
        Increment Notification
      </button>
    </>
  )
}
export default Profile

I would not recommend this approach. At some point, other developers may change the dependency array and cause the same remounting issue, turning code that was initially intended to be easier to read and organized into a potential bottleneck.

It might seem tempting to take this risk and think, When I realize that I need to use a state or prop, I'll just change it. or We have code review, it's ok

However, be aware that there will always be the risk of some developers thinking they need to pass the state to the dependency array, and that problem will only grow as that antipattern is used in several components of the application.

Class Component

When working with classes, we don't have that mounting issue, as only the render method is executed when a re-render occurs :)

Overall

All of this can be avoided by creating a components folder or another similar name and importing these small components. This would make the code simpler, more performant, and make life easier for those who are starting out in the project or in their career.

In the end, I believe that all this effort to work around the negative effects of an antipattern in the project is not worth it. It would be better to spend this time and effort on things that would bring more value to the end customer or the team.

I hope this post has been helpful. See you! :)

Follow me on Social Media

Instagram:

@colaquecez.dev

Share it ;)