Say you are trying to track down why a child component in your React tree keeps re-rendering when the parent updates state that has nothing to do with it. You open React DevTools Profiler, click around, and confirm the child is rendering far more often than it should. Then you look at the code and see an inline arrow function passed as a prop:

<Child onClick={() => handleSave()} />

That () => handleSave() is a new function reference on every single render of the parent. React sees a different onClick prop each time, so the child re-renders even if its internal state and other props stayed the same. But here is the question that matters: how much does that reference churn cost in practice? Is it always a problem worth fixing, or is it one of those rules that sounds correct in theory but rarely shows up in the profiler?

Q: What does “new reference on every render” mean for performance?

It means two things happen on each parent render, regardless of whether the props’ values changed.

First, the inline object or function is allocated in memory. For a single prop, this allocation is trivial — a few bytes, microseconds of work. Second, React’s reconciliation compares the old prop reference with the new one. Since the reference is new, the comparison fails, and React schedules the child for re-render. The child then runs its own render function, diffs its virtual DOM, and potentially commits DOM updates if anything changed.

The cost is not in the allocation itself. The cost is in the child’s entire render cycle — running its component body, re-evaluating JSX, and reconciling its children — all for a prop that holds the same value as last time.

Q: So when does this measurably hurt?

In testing across several component trees, the negative impact scales with three factors: render frequency, subtree size, and render cost per node.

If the parent renders once per second and the child is a simple <span> that renders in under a millisecond, the inline function cost is noise. You will not see it in the Profiler’s flamegraph. The browser can absorb thousands of those renders per frame without dropping below 60fps.

The picture changes when the parent re-renders at high frequency — say, tied to a slider input, a drag operation, or websocket-driven state updates — and the child is a list of dozens of rows, each with its own components and hooks. In that setup, an inline function passed down to a memoized list item will defeat the memoization entirely. The profiler shows the entire list re-rendering on every slider tick, even though only the parent’s local state changed.

I profiled a table component with fifty rows, each row wrapped in React.memo. Passing an inline onSort function to the table header caused every row to re-render on each sort-state change. Replacing it with useCallback dropped the per-interaction render count from fifty-plus renders down to one — the table header re-rendered, the rows did not.

Q: Is it always the child that pays the price?

No. Inline objects have an additional cost that inline functions do not: they defeat React.memo and PureComponent equal checks, forcing the child to render, and then within that render, any downstream consumers of the object also see a new reference. If the child passes the object down to grandchildren via context or props, the churn propagates down the tree.

Consider this pattern:

function Parent() {
  const config = { theme: 'dark', compact: true };
  return <Child config={config} />;
}

Child re-renders on every parent update. If Child is memoized, the memo fails because the object reference changed. If Child has its own children that also consume config, those re-render too. The reference instability compounds down the tree, turning a single parent update into a cascade of renders that each produce no visual change.

Q: Does useCallback and useMemo always fix the problem?

They fix the reference instability, provided the dependency arrays are correct. But they introduce their own costs. useCallback and useMemo add a tiny amount of overhead per render — the hook must compare the dependency array. For most components, this overhead is negligible.

The more significant issue is dependency array mistakes. A common error looks like this:

const handleClick = useCallback(() => doSomething(id), []);

If id changes, handleClick keeps the old closed-over value. That is a correctness bug, not a performance issue, but it hides the same class of problem: the callback reference is stable, yet the behavior is stale. Fix with the correct deps: [id].

The other consideration: memoizing a callback that the child does not need to be memoized against adds syntactic noise without measurable benefit. If the child is cheap to render and the parent re-renders rarely, useCallback is ceremony. The profiler will show zero difference.

Q: What does the Profiler show for inline vs. memoized props?

In a controlled benchmark with a parent rendering fifty times per second, passing a fresh object to a memoized child, the Profiler shows the child’s render duration as roughly 0.5–1.5 ms per render. That does not sound like much, but over a second of interaction, that is fifty to seventy-five milliseconds of render work — time the main thread could have spent on input handling or painting.

After applying useMemo to the object, the same profiler session shows the child rendering zero times per second during identical parent activity. The parent still re-renders fifty times, but the child’s render time becomes 0 ms. The cumulative difference over a ten-second interaction is a few hundred milliseconds of CPU time — measurable in real-world responsiveness, especially on lower-end mobile devices.

For extremely cheap components, the numbers reverse. A simple text node re-rendering fifty times per second costs less than 0.1 ms per render, totaling about five milliseconds per second. That is below the threshold where a human would notice anything. In that scenario, adding useCallback and useMemo is not wrong, but the Profiler will not reward you for it.

Q: What about inline functions passed to event handlers — is that different?

Event handlers take a slightly different path. An inline onClick on a DOM element — <button onClick={() => setCount(c => c + 1)}> — does not trip the same reference comparison because React attaches the event listener to the root container and delegates. The new function reference is stored, not compared against a previous value for memoization purposes.

The cost appears only when that function is passed down to a custom component that sits between the parent and the DOM. If the custom component is memoized, the inline function defeats the memo. If it is not memoized, the function reference churn has no direct cost — the child re-renders because its parent re-rendered, not because of the prop comparison.

That distinction matters. Wrapping an event handler in useCallback solely because it is inline, when the receiving component is not memoized, is wasted effort. The Profiler will show identical render counts.

Q: When should you reach for useCallback and useMemo for props?

The decision framework reduces to three checks, ordered by impact:

  1. Is the receiving component memoized? If you see React.memo, PureComponent, or a memoized child, references passed from the parent must be stable for the memo to work. This is the highest-impact case.
  2. How expensive is the child’s render tree? A child with hooks, context consumers, or a large list of descendants amplifies the cost of each unnecessary render. The more expensive the subtree, the more a skipped render saves.
  3. How often does the parent re-render? High-frequency parents — animations, drag-and-drop, live-updating data — turn a small per-render cost into a sustained main-thread load. Low-frequency parents render so rarely that reference churn is functionally irrelevant.

If all three align — memoized child, expensive subtree, frequent parent updates — then useCallback and useMemo are the correct tools. If none align, leaving the inline function is fine.

Q: Are there cases where inline objects cause issues beyond the render tree?

Yes, when they are used in effect dependencies or in context values. An inline object in a useEffect dependency array forces the effect to re-run on every render:

useEffect(() => {
  fetchData(config);
}, [config]); // config is a new object every render

That creates a network request on every render — far more expensive than any render work the object caused. The fix is useMemo for the object, or restructuring the effect to depend on primitive values inside the object.

Context values follow the same rule. Passing { user, theme } inline as a context value re-renders every consumer on every provider render, regardless of whether the underlying data changed. The React team’s guidance is to memoize context values with useMemo for this exact reason.

Q: What does a straightforward fix look like in practice?

Here is a before-and-after pair for a memoized list row:

// Before: defeats React.memo on each parent render
function List({ items, onDelete }) {
  return items.map(item => (
    <Row key={item.id} item={item} onDelete={() => onDelete(item.id)} />
  ));
}
// After: stable callback, memoized per item
function List({ items, onDelete }) {
  return items.map(item => (
    <Row
      key={item.id}
      item={item}
      onDelete={useCallback(() => onDelete(item.id), [onDelete, item.id])}
    />
  ));
}

The hook rules require calling useCallback at the top level, so the “after” version is not valid as written — it illustrates the idea but needs restructure. A correct version splits the callback into a child component or uses useCallback in a map with a dedicated component:

function List({ items, onDelete }) {
  return items.map(item => (
    <Row
      key={item.id}
      item={item}
      onDelete={() => onDelete(item.id)}
    />
  ));
}

function Row = React.memo(function Row({ item, onDelete }) {
  return (
    <div>
      <span>{item.name}</span>
      <button onClick={() => onDelete(item.id)}>Remove</button>
    </div>
  );
});

Here onDelete is still an inline function passed from List. But since Row is memoized and receives item (a stable reference from the parent’s state) and onDelete (a new reference each render), the memo fails for onDelete. The fix is to use useCallback in List outside the map:

function List({ items, onDelete }) {
  const handleDelete = useCallback(
    id => onDelete(id),
    [onDelete]
  );
  return items.map(item => (
    <Row key={item.id} item={item} onDelete={handleDelete} />
  ));
}

Now handleDelete is stable across renders, and Row’s memo works as intended.

Q: How do I verify the change worked?

The only reliable verification is the React DevTools Profiler. Record a session before the change, trigger the parent’s re-render, and note the child’s render count. Apply the fix, record again, and compare. A working memoization change shows the child’s render count drop to zero or near-zero while the parent continues to re-render.

Visual inspection of the code is not enough — dependency array mistakes, stale closures, or an extra state update elsewhere can silently keep the child rendering. The Profiler is the source of truth.

One additional check: search for whether the child is being passed a new object or function from anywhere in the parent, including context providers or render props. A single unstable reference traced through a context provider defeats memoization at every level below it.

Quick Reference: When Inline Props Matter

Scenario Impact Recommended Action
Memoized child receives inline function/object High — memo fails, child re-renders Stabilize with useCallback / useMemo
Non-memoized child receives inline prop Negligible — child re-renders anyway No change needed
Inline object in useEffect deps High — effect re-runs every render Memoize the object or use primitives in deps
Inline object as context value High — all consumers re-render Memoize the context value
Inline function on DOM element Negligible — no memo comparison No change needed
High-frequency parent + expensive subtree High — cumulative main-thread load Stabilize references
Low-frequency parent + cheap subtree Negligible — cost below perception No change needed

The guiding principle is measurement over assumption. Inline functions and objects are not inherently bad; they become problematic when they sit between a frequently re-rendering parent and a memoized, expensive subtree. Run the Profiler, find the render being wasted, and apply useCallback or useMemo only where the data says it matters.