skip to content

Handler Identity and Inline Functions

An inline arrow handler is a brand-new function on every render, which is fine most of the time and expensive occasionally. Interviewers ask this to hear a measured answer about referential identity and stale closures rather than a reflexive 'always use useCallback'.

part ofReactoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In React, an inline handler such as onClick={() => setOpen(true)} is a brand-new function on every render. Why does that happen, and when is it actually a problem?

level: juniorimportance: must knowfreq 78%

answer

  1. the body runs again each render
  2. identical source, different object
  3. allocation is cheap, identity is not
  4. only matters where something compares
  5. memo boundaries and dependency arrays

basics

~20 s

Every render re-runs the component body, so the arrow expression evaluates again into a new function object. The allocation itself is cheap; the new identity only matters where something compares it, such as a memoized child or a dependency array.

solid answer

~50 s

A function component's body re-runs on every render, and a function expression evaluates to a new object each time it is evaluated, so `() => setOpen(true)` is a different function on each pass — two closures with identical source are never `===`. That allocation is genuinely cheap: engines create closures constantly, and rendering the component costs far more than the closure does. What can matter is the *identity*. If the handler is passed to a child wrapped in `React.memo`, the shallow prop comparison sees a changed prop and the child re-renders anyway, so the memo is inert. If the handler sits in a dependency array, the effect or memo re-runs every render. React does not re-attach a DOM listener for the new function, so there is no listener churn. Reach for `useCallback` where one of those comparisons actually exists, not by reflex.

code

javascript · 11 lines
javascript
import { useState } from 'react';

function Toggle() {
  const [open, setOpen] = useState(false);
  const a = () => setOpen(o => !o);
  const b = () => setOpen(o => !o);
  console.log(a === b); // false - two evaluations, two objects
  return <button onClick={a}>{open ? 'Hide' : 'Show'}</button>;
}

export default Toggle;

go deeper

for a junior

Be ready to say plainly that the component body re-runs on every render, so the arrow expression produces a new function object each time, and that this is normal rather than a bug.

for a middle

Explain the mechanics: function expressions evaluate to new objects, React stores the handler on the component node instead of re-binding DOM listeners, and identity only matters where Object.is comparisons happen.

for a senior

Show the judgment an interviewer wants: separate allocation cost from identity cost, name the exact boundaries where identity bites, and refuse to memoize by reflex without a comparing consumer.

for a principal

Own the codebase-wide position: whether the team hand-memoizes at all, whether the React Compiler makes that policy obsolete, and how you keep a rule like this from becoming cargo-culted ceremony in reviews.

## A render is just a function call A React function component is a plain function that React calls to produce elements. React calls it on mount, on every state update, and whenever the parent re-renders. Each call executes the whole body from top to bottom, so everything the body evaluates is evaluated again: local `const` bindings, object literals, array literals, and function expressions. ```jsx function Toggle() { const [open, setOpen] = useState(false); const handleClick = () => setOpen(o => !o); // new object every render return <button onClick={handleClick}>{open ? 'Hide' : 'Show'}</button>; } ``` Writing it inline as `onClick={() => setOpen(o => !o)}` is the same thing without a name. In JavaScript a function expression produces a fresh function object each time it is evaluated, and two functions are equal only when they are the same object — identical source text does not make them `===`. So on render 5 the handler is a different value than the handler on render 4, even though nothing about the component changed. ## What React does with the new function The handler is a prop on the element you returned. React stores it on the component's internal node and dispatches to whatever is currently stored there when the event fires. It does not call `removeEventListener` and `addEventListener` again because you handed it a new function — replacing the handler is a field write, not DOM work. This is why the common worry ("I am re-binding listeners on every keystroke") is misplaced. ## Two costs that get confused Be precise about which cost you are talking about, because interviewers are listening for exactly this distinction. **Allocation cost.** Creating one closure is on the order of nanoseconds. A component that renders a few dozen elements does far more expensive work building the element objects than it does allocating handlers. For the overwhelming majority of components this cost is invisible and optimizing it is a waste of effort. **Identity cost.** This one is real, but it only appears where some piece of code compares the previous value with the new one using `Object.is` semantics. There are three places that happens: - `React.memo`, which shallowly compares the child's props and skips the re-render only if every prop is unchanged. A fresh handler prop makes the comparison fail, the child re-renders every time, and the `memo` wrapper buys nothing while still costing a comparison. - Dependency arrays of `useEffect`, `useMemo`, and `useCallback`. A handler listed as a dependency changes every render, so the effect re-runs every render — which turns a subscription or a fetch into a loop. - Anything that keys off the value, such as a context value object built from the handler, which then invalidates every consumer. ```jsx const Child = memo(function Child({ onSave }) { /* ... */ }); function Parent() { // Child re-renders on every Parent render: onSave is a new object each time return <Child onSave={() => save()} />; } ``` ## Stabilising identity, and when not to `useCallback(fn, deps)` returns the same function object across renders for as long as the dependencies compare equal, which is how you give a memoized consumer a stable prop. The important discipline is that stabilising a handler is only useful when it is paired with a consumer that compares. `useCallback` on a handler passed to an ordinary, unmemoized child changes nothing: that child re-renders whenever its parent renders, regardless of prop identity. It is also not free. You are adding a hook call, a dependency array to keep honest, and a closure that React retains between renders. Applied everywhere it makes code noisier and no faster. In React 19 setups that enable the React Compiler (`babel-plugin-react-compiler`), this memoization is inserted at build time for code that follows the Rules of React, which removes most of the hand-written `useCallback` calls that used to exist purely to keep a handler stable. ## What to say in an interview The measured answer is: yes, it is a new function every render; that is how JavaScript evaluation works and the allocation does not matter; identity matters only at a comparison boundary; so the fix is targeted, not global. Candidates who answer "always wrap handlers in `useCallback`" have memorised a rule instead of the mechanism, and the follow-up ("what does that do if the child is not memoized?") usually exposes it.

  • Does giving a button a new onClick function each render make React detach and re-attach a DOM listener?
    No. React keeps the handler on the component's internal node and looks it up when the event is dispatched, so swapping the prop is a field write rather than DOM listener work. The "I am re-binding listeners every keystroke" fear is unfounded — the real cost of a fresh handler is its identity failing a comparison somewhere, not DOM churn.
  • If the inline handler is passed to a plain child component that is not wrapped in React.memo, what does the fresh identity cost you?
    Effectively nothing. An unmemoized child re-renders whenever its parent re-renders, whatever its props look like, so a stable handler would not have prevented that render. Wrapping it in `useCallback` there adds a hook and a dependency array while changing no behaviour. The identity only starts paying once the consumer actually compares props or dependencies.
  • How does the React Compiler change this advice in React 19?
    The React Compiler (`babel-plugin-react-compiler`) inserts memoization at build time for components that follow the Rules of React, so handlers and derived values get stable identities without hand-written `useCallback`. You still need to understand the mechanism to reason about code the compiler cannot handle, and to read the large body of existing code full of manual memoization.

saying these in an interview costs you the question

  • Claims inline arrow handlers cause noticeable slowdowns in ordinary UIs
  • Says React re-attaches a DOM listener when the handler prop changes
  • Wraps every handler in useCallback without a memoized consumer
  • Thinks React compares handler functions by their source text
  • Believes a stable handler stops an unmemoized child from re-rendering

context

open as a page

In React JSX, what is the difference between writing onClick={handleClick} and onClick={handleClick()}, and what actually happens with the second form?

level: juniorimportance: must knowfreq 72%

basics

~20 s

The first passes the function so React can call it on click. The second calls it during render and passes its return value, usually undefined, so nothing happens on click and the side effect fires on every render.

open as a page

A React counter's click handler is created as useCallback(() => setCount(count + 1), []). The number goes to 1 and then stops changing. What is happening, and how do you fix it?

level: middleimportance: must knowfreq 64%

basics

~20 s

The empty dependency array pins the handler to the first render, where count was 0, so every click computes 0 + 1. Fix it with the updater form setCount(c => c + 1), or by listing count as a dependency.

open as a page

A React list renders each row with onClick={() => onSelect(item.id)}, creating one closure per row on every render. What are the alternatives that avoid that, and what do you trade for them?

level: middleimportance: should knowfreq 46%

basics

~20 s

Push the binding into the row component so the parent passes one stable callback plus the id, or render a single shared handler and read the id from a data attribute on the clicked element. Both trade convenience for a stable function identity.

open as a page

A teammate wraps every event handler in a React component in useCallback to "reduce re-renders". When does stabilising a handler's identity actually prevent a render, and when does it change nothing?

level: seniorimportance: should knowfreq 54%

basics

~20 s

A stable handler prevents a render only where something compares it: a child wrapped in React.memo, or a dependency array. Passed to an ordinary child it changes nothing, because that child re-renders whenever its parent does.

open as a page