DEV Community

Sergio Pedercini
Sergio Pedercini

Posted on

The Hidden Cost of Object Props in React

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> ); } 
Enter fullscreen mode Exit fullscreen mode

Now imagine we use it like this:

function Parent() { return ( <UserCard user={{ name: 'Alice', avatarUrl: '/alice.png' }} /> ); } 
Enter fullscreen mode Exit fullscreen mode

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>; } 
Enter fullscreen mode Exit fullscreen mode

Now look at the parent:

<MapPin position={{ lat: 45.0, lng: 9.0 }} /> 
Enter fullscreen mode Exit fullscreen mode

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> ); } 
Enter fullscreen mode Exit fullscreen mode

And the usage:

<Form config={{ showEmail: true, showPhone: false }} /> 
Enter fullscreen mode Exit fullscreen mode

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" />; 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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} />; } 
Enter fullscreen mode Exit fullscreen mode

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)