Fixing Infinite Re-renders When You Pass Object Literals as React Props
Your component re-renders, passes an object literal as a prop, the child re-renders, triggers a useEffect, updates state, and the whole thing starts over. Within milliseconds your browser tab is frozen and the console is screaming Too many re-renders. The culprit is almost always a reference equality problem with an object you created inline.
This is one of those bugs that looks completely innocent in the code but causes catastrophic runtime behavior. Understanding the exact mechanism makes it trivially easy to fix β and to avoid in the future.
What you'll learn
- Why JavaScript reference equality makes inline objects dangerous as props
- How React's re-render cycle turns a single object literal into an infinite loop
- Five concrete fixes ranked by situation: moving objects outside components,
useMemo,useRef, flattening props, andReact.memo - The common mistakes that keep the loop alive even after you think you've fixed it
The Root Cause: Reference Equality in JavaScript
JavaScript compares objects by reference, not by value. Two objects that look identical are not equal unless they point to the same place in memory.
const a = { color: 'red' };
const b = { color: 'red' };
console.log(a === b); // false β different references
console.log(a === a); // true β same reference
This is not a React quirk. It is how JavaScript works at its core. React simply exposes the consequences more dramatically than most code you write day-to-day.
How React Decides to Re-render
When a parent component re-renders, React calls the parent function again from top to bottom. It then compares each prop it is about to pass to a child against the prop it passed on the previous render. The comparison is a strict equality check (===) on the prop value itself.
For primitives like strings and numbers, this is fine. 'red' === 'red' is always true. But for objects, React is comparing references, and a brand-new object literal created inside the render function gets a brand-new reference every single time.
If the child component has a useEffect that lists that object in its dependency array, React sees a new object on every render and fires the effect every render. If that effect sets state, the parent re-renders again β and so it goes, forever. You can read more about how React's development mode adds another layer to this problem in this breakdown of why useEffect fires twice in dev but once in production.
Why Object Literals Are the Silent Culprit
The dangerous pattern looks completely natural, which is why it catches so many developers off guard.
function ParentComponent() {
const [count, setCount] = React.useState(0);
return (
<ChildComponent
config={{ theme: 'dark', size: 'large' }} {/* new object every render */}
onClick={() => setCount(c => c + 1)}
/>
);
}
function ChildComponent({ config, onClick }) {
React.useEffect(() => {
console.log('config changed:', config);
// do something with config...
}, [config]); // fires every render because config is always a new reference
return <button onClick={onClick}>Click</button>;
}
Every time ParentComponent renders β for any reason β it creates a new { theme: 'dark', size: 'large' } object. Even though the values are identical, the reference is new. React dutifully fires the useEffect, and if that effect triggers a state update anywhere up the tree, you have your loop.
The same problem applies to inline arrays ([]), inline functions (() => {}), and inline class instances. Anything that gets constructed fresh inside a render function body.
Spotting the Infinite Loop in Practice
React will throw Error: Too many re-renders. React limits the number of renders to prevent an infinite loop when it detects the problem synchronously. But often the loop is more subtle β the component keeps re-rendering at high frequency without crashing, burning CPU cycles and making the UI sluggish.
To diagnose it, open React DevTools and use the Profiler tab. Enable Record why each component rendered before starting a profiling session. Trigger the interaction that causes the repeated renders, then inspect the flame graph.
If the child component repeatedly shows:
Props changed:
config
filters
options
even though the values appear identical, you've likely found a reference equality problem.
The profiler helps distinguish between components that legitimately re-render because their data changed and those that re-render because new object references are being created on every pass.
Fix 1: Move Static Objects Outside the Component
If the object never changes, don't recreate it inside the component.
Instead of:
function Parent() {
return (
<Child
config={{
theme: "dark",
size: "large"
}}
/>
);
}
Move it outside:
const DEFAULT_CONFIG = {
theme: "dark",
size: "large"
};
function Parent() {
return (
<Child config={DEFAULT_CONFIG} />
);
}
Now every render uses the exact same object reference.
This is the simplest and most efficient solution for constants.
Fix 2: Memoize Dynamic Objects with useMemo
Sometimes the object depends on props or state.
Instead of recreating it:
const config = {
theme,
size
};
use:
const config = React.useMemo(() => ({
theme,
size
}), [theme, size]);
Now React only creates a new object when either dependency actually changes.
This is the most common solution for configuration objects passed to child components.
Fix 3: Store Stable References with useRef
Occasionally an object should never change after initialization.
Example:
const config = React.useRef({
timeout: 5000,
retries: 3
});
Use:
config.current
throughout the component.
Unlike useMemo, useRef doesn't recreate values when dependencies change.
It's ideal for mutable objects that shouldn't trigger re-renders.
Fix 4: Flatten Props Instead of Passing Objects
Sometimes you don't need an object at all.
Instead of:
<Child
settings={{
darkMode,
compact
}}
/>
pass:
<Child
darkMode={darkMode}
compact={compact}
/>
Primitive values compare much more predictably than object references.
This also makes components easier to understand and test.
Fix 5: Combine Stable Props with React.memo
Even stable objects won't prevent unnecessary rendering if the child itself always re-renders.
Wrap the child:
const Child = React.memo(function Child(props) {
...
});
React now performs a shallow comparison of props before rendering.
Combined with stable object references, this can dramatically reduce unnecessary work.
Arrays Have the Same Problem
Inline arrays behave exactly like objects.
Avoid:
<Chart
colors={["red", "blue", "green"]}
/>
Instead:
const colors = React.useMemo(() => [
"red",
"blue",
"green"
], []);
Every render previously created a new array reference.
Memoization fixes it.
Inline Functions Can Trigger Similar Issues
Functions are also objects in JavaScript.
Avoid:
<Child
onSave={() => saveItem(id)}
/>
when the child depends on callback identity.
Instead:
const handleSave = React.useCallback(() => {
saveItem(id);
}, [id]);
useCallback stabilizes the function reference between renders.
Watch Your useEffect Dependencies
This effect:
useEffect(() => {
fetchData(config);
}, [config]);
only behaves correctly if config itself is stable.
Otherwise:
Render
β
New object
β
Effect runs
β
State update
β
Render
β
New object
β
Effect runs
The dependency array isn't broken.
The dependency reference is.
Memoization Isn't a Magic Fix
Developers sometimes write:
const config = useMemo(() => ({
theme,
options
}), [theme]);
Notice the mistake.
options isn't listed.
Now the memoized object becomes stale.
Always include every value used inside the memo callback.
Ignoring dependency warnings often creates harder bugs than the one you're trying to solve.
Deep Objects Need Extra Care
Suppose:
const config = useMemo(() => ({
theme,
filters
}), [theme, filters]);
If filters itself changes reference every render, the memo becomes ineffective.
Sometimes you must stabilize nested objects first.
Work from the deepest changing reference outward.
Avoid Memoizing Everything
useMemo has a cost.
Avoid wrapping trivial values:
const title = useMemo(() => "Dashboard", []);
There's no benefit.
Reserve memoization for:
- Objects
- Arrays
- Expensive computations
- Derived data
- Callback functions passed to children
State Updates Inside Effects
Another subtle loop:
useEffect(() => {
setConfig({
theme: "dark"
});
}, []);
Looks harmless.
But if another effect depends on config and recreates another object, the cycle can continue.
Review every state update involving objects.
Custom Hooks Can Hide the Problem
Example:
const config = useSettings();
If useSettings returns:
return {
theme,
language
};
without memoization,
every consumer receives a new object every render.
Custom hooks should also return stable references whenever practical.
React Strict Mode Makes It More Noticeable
In development, React Strict Mode intentionally invokes certain lifecycle behavior twice to help uncover side effects.
This often makes unstable references appear much worse during development.
If your application only "breaks" in development, don't disable Strict Mode immediately.
Instead, investigate whether unstable objects, arrays, or callbacks are causing repeated effects.
Fixing the underlying reference issue usually resolves the development behavior as well.
Common Mistakes
Memoizing the Wrong Object
Stabilize the object being passedβnot an unrelated value.
Forgetting Dependencies
Incomplete dependency arrays create stale values.
Passing Nested Inline Objects
Even if the outer object is memoized, nested objects can still change every render.
Assuming React.memo Solves Everything
React.memo only works when incoming props remain referentially stable.
Creating Objects Inside JSX
Inline object literals always create fresh references.
Choosing the Right Fix
| Situation | Recommended Solution |
|---|---|
| Constant configuration | Move object outside the component |
| Object depends on state or props | useMemo |
| Mutable object that should never trigger renders | useRef |
| Passing many simple values | Flatten props |
| Prevent unnecessary child renders | React.memo with stable props |
| Callback passed to children | useCallback |
Selecting the appropriate technique keeps components predictable while avoiding unnecessary optimization.
Debugging Checklist
When you encounter unexpected re-renders:
- Enable React DevTools Profiler and record why components render.
- Look for object, array, or callback props changing every render.
- Search for inline object literals inside JSX.
- Verify
useMemoanduseCallbackdependency arrays. - Check custom hooks for newly created objects.
- Confirm child components wrapped with
React.memoreceive stable references. - Review every
useEffectdependency that contains objects or arrays.
Following this checklist usually identifies the source of infinite or excessive re-renders within minutes.
Final Thoughts
Infinite re-render loops caused by inline object literals aren't a React bugβthey're the natural consequence of JavaScript's reference equality rules combined with React's rendering model. Every time a component renders, new object and array literals receive new memory references, and React correctly treats them as changed props. When those changing references appear in dependency arrays or memoized components, seemingly harmless code can quickly spiral into excessive rendering or even an infinite loop.
The solution isn't to avoid objects altogether, but to keep their references stable. Moving constants outside components, using useMemo for derived objects, storing long-lived values in useRef, flattening props where appropriate, and combining these techniques with React.memo creates components that are both efficient and predictable. Understanding when React compares values versus references will help you eliminate an entire class of rendering bugs before they ever reach production.
Frequently Asked Questions
Why does passing an object as a prop cause a React component to re-render infinitely?
Every time a parent component renders, an object literal written inside the JSX gets a new memory reference, even if the values inside it haven't changed. React compares props with strict equality, so it always sees a 'new' prop and re-renders the child. If the child's useEffect depends on that prop, the effect fires again and can trigger more state updates, creating an infinite loop.
Does wrapping a child in React.memo stop infinite re-renders caused by object props?
Not on its own. React.memo uses shallow equality, which compares object props by reference. If the parent creates a new object literal on every render, React.memo sees a new reference and re-renders the child anyway. You need to also stabilize the object in the parent using useMemo so the reference only changes when the data actually changes.
How do I know if a useMemo dependency array is correct for an object?
List every variable from the component scope that is read inside the useMemo callback. If any of those variables change and your object should reflect that change, they must be in the dependency array. An empty array means the object never updates, which is usually wrong β move a truly static object to module scope instead.
Can inline arrays cause the same infinite re-render problem as inline objects?
Yes. Arrays are objects in JavaScript and are compared by reference, not by value. An inline array like columns={['name', 'email']} creates a new reference on every render and causes the exact same problem. Apply the same fixes: define it at module scope if it's static, or wrap it in useMemo if it depends on component state or props.
What is the fastest way to find which prop is causing an infinite re-render?
Open the React DevTools Profiler, record a session, and look for components that re-render continuously with no user interaction. Click into those renders to see which props changed between renders. The prop showing a different value on every frame β especially an object or function β is the culprit.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!