The Temptation
You’re building a dashboard and need a profile card for the logged-in user.
It’s quick, it’s simple, so you write:
function Dashboard({ user }) {
function ProfileCard() {
return (
<div className="card">
<h2>{user.name}</h2>
<p>{user.role}</p>
</div>
);
}
return (
<div>
<h1>Dashboard</h1>
<ProfileCard />
</div>
);
}
It works. The UI renders. You move on. 🚀
But lurking under the surface is a performance and state bug just waiting to bite.
The Hidden Problem
Every time Dashboard re-renders, whether because user changes or some unrelated state updates, React recreates the ProfileCard function.
That one fact breaks React’s reconciliation process and can lead to:
- State loss inside ProfileCard
- Extra unmount/mount cycles
- Wasted DOM operations
Quick Refresher: What is Reconciliation?
Reconciliation is how React updates the UI without throwing everything away.
Steps simplified:
- Build a new virtual DOM tree from your latest render.
- Compare it to the previous virtual DOM tree.
- Update only the parts that changed in the real DOM.
For React to do this efficiently, it relies heavily on component identity.
How React Tracks Component Identity
In the virtual DOM, each element has a type property:
- Strings for DOM nodes (
'div','span', etc.) - Functions or classes for React components
Example:
{
type: ProfileCard, // function reference
key: null,
props: { name: "Jane" }
}
During reconciliation, React compares the type of the old element and the new element.
The type is the function or class reference for a component, or a string for a DOM element (like 'div' or 'span').
If the type changes, React assumes:
“This is a completely different component.”
When that happens, React will:
- Unmount the old component (run cleanup effects, discard state).
- Mount the new one from scratch (run the function body fresh, initialize hooks).
The Problem with Inline Components
When you define a component inside another component:
function Dashboard() {
function ProfileCard() { /* ... */ }
return <ProfileCard />;
}
React creates a brand-new component on every render.
- On render #1,
type=ProfileCard@0x001(memory address A) - On render #2,
type=ProfileCard@0x002(memory address B)
They may look the same in code, but they are different function objects in memory.
React’s check:
oldFiber.type !== newFiber.type // true
And that triggers a full unmount + mount.
Result:
- Any local state in
ProfileCardis lost. - Effects run cleanup + re-run from scratch.
- DOM nodes are destroyed and recreated.
Real-World Impact
Let’s say ProfileCard has a form for editing user info:
function ProfileCard() {
const [editing, setEditing] = useState(false);
// form logic...
}
If Dashboard re-renders due to a theme change or notification count update:
- Old
ProfileCardis unmounted. - New
ProfileCardis mounted. -
editingresets tofalse. - Your user loses unsaved changes. 😬
The Correct Approach
Move ProfileCard out so its identity is stable:
function ProfileCard({ name, role }) {
return (
<div className="card">
<h2>{name}</h2>
<p>{role}</p>
</div>
);
}
function Dashboard({ user }) {
return (
<div>
<h1>Dashboard</h1>
<ProfileCard name={user.name} role={user.role} />
</div>
);
}
Now:
-
ProfileCardkeeps the same reference across renders. - React preserves state and DOM.
Takeaway
React’s reconciliation is built on stable component identity.
Defining components inside other components breaks that stability, causing unnecessary unmounts and state loss.
Top comments (0)