React is a great library: easy to get started with, backed by a huge community, and extremely flexible. But that flexibility can also hide pitfalls that bite you later, especially when it comes to performance.
One of the most common (and subtle) mistakes you can make is passing objects as props, without thinking about how React tracks changes and re-renders components.
Let’s dig into why this is a problem, and how to avoid it with clear, simple examples.
Why passing objects can hurt performance
React decides whether to re-render a component by checking whether its props or state have changed. For props, it does a shallow comparison, meaning it compares values by reference, not by content.
So if you pass an object like { name: 'Alice' }
, React will see it as a “new value” every time you recreate it, even if it has the same contents. That’s how you accidentally trigger unnecessary re-renders.
1. A basic example: a simple UserCard
component
Here’s a basic component that takes a user
object:
function UserCard({ user }: { user: { name: string; avatarUrl: string } }) { return ( <div> <h1>{user.name}</h1> <img src={user.avatarUrl} alt={user.name} /> </div> ); }
Now imagine we use it like this:
function Parent() { return ( <UserCard user={{ name: 'Alice', avatarUrl: '/alice.png' }} /> ); }
This looks fine, right?
But here's the problem: every time Parent
renders, it creates a new object for the user
prop, even though it has the same values. React will think user
has changed, and re-render UserCard
, even if nothing really changed.
This adds up over time, especially if you're rendering a list of users or using this pattern throughout the app.
2. A real cost example: a MapPin
component
Let's now look at a more practical use case: a MapPin
component that resets the centre of the map when the position changes.
function MapPin({ position }: { position: { lat: number; lng: number } }) { useEffect(() => { map.flyTo(position); // Set the centre of the map to the pin }, [position]); return <div>📍</div>; }
Now look at the parent:
<MapPin position={{ lat: 45.0, lng: 9.0 }} />
Even if lat
and lng
are the same across renders, this still creates a new object every time, so position
is seen as "changed", and the useEffect
will re-trigger.
In this case, you’re not just causing a re-render, you’re causing side effects, like the map jumping around unnecessarily.
3. Passing a config
object to a Form
Let’s say we have a configurable form component:
function Form({ config }: { config: { showEmail: boolean; showPhone: boolean } }) { return ( <form> {config.showEmail && <input type="email" />} {config.showPhone && <input type="tel" />} </form> ); }
And the usage:
<Form config={{ showEmail: true, showPhone: false }} />
Same story: this inline object causes React to think the prop has changed on every render, which breaks memoization and can cause wasted updates or performance issues, especially in large forms or reusable UI libraries.
How to fix it
There are several ways to avoid this issue:
1. Split object props into primitive values
The most straightforward and reliable solution is to avoid passing objects entirely:
function UserCard({ name, avatarUrl }: { name: string; avatarUrl: string }) { return ( <div> <h1>{name}</h1> <img src={avatarUrl} alt={name} /> </div> ); } // Usage <UserCard name="Alice" avatarUrl="/alice.png" />;
This completely sidesteps the problem of referential equality, and keeps your component interface simple and predictable.
2. Use composition instead of props
Instead of passing structured data like a config or image object, let the parent render those elements directly:
function UserCard({ name, children }: { name: string; children: React.ReactNode }) { return ( <div> <h1>{name}</h1> {children} </div> ); } // Usage <UserCard name="Alice"> <UserCard.Image url="/alice.png" alt="Alice" /> </UserCard>
3. Use useMemo
to preserve reference
If you must build an object dynamically, and especially if you're passing it to a memoized component or it's expensive to compute, useMemo can help stabilize the reference:
function Parent({ userId }: { userId: string }) { const user = useMemo(() => { return { avatarUrl: `/avatars/${userId}.png`, name: userId.toUpperCase(), }; }, [userId]); return <UserCard user={user} />; }
Important note: useMemo
is not a silver bullet. It adds mental overhead and can be misused. If you’re reaching for it just to "fix" an object prop you created yourself, it might be a sign that the design should be simplified instead.
Final thoughts
Passing objects as props in React might seem convenient, but it introduces a subtle and dangerous problem: React compares props by reference, not by value. Even if the contents are identical, every new object is treated as different, leading to unnecessary re-renders and broken memoization.
You can memoize them using useMemo
or move the object outside the render function to work around the issue, but these cannot be a long term solutions: you have to remember to memoize, track dependencies, avoid inline objects, and that’s hard to enforce in real teams and fast-moving projects.
You shouldn't be afraid of objects, but be careful with them.
Before using objects, try to:
- split props into primitives (e.g. name, avatarUrl);
- use composition to pass JSX or render logic from the parent.
These patterns lead to more predictable behavior, cleaner code, and better performance, without having to rely on memoization hacks.
If you're thinking long-term, especially as your components grow or your team scales, favor simplicity over cleverness. Composition isn't just safer, it's idiomatic React.
Top comments (0)