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.
DEEP DIVE
UI Tree? Same position, same state?
DEEP DIVE
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! :)