skip to content

Event Handling

React does not attach your onClick to the element you wrote it on. This group covers the synthetic event wrapper, where listeners really live, and how handler identity interacts with re-renders — three questions that separate framework users from framework understanders.

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

explore

questions

14

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

In a React onSubmit or onClick handler, why does `return false` not stop the browser's default behaviour, and what do you write instead?

level: juniorimportance: must knowfreq 50%

basics

~10 s

React ignores a handler's return value entirely. Returning false only cancels defaults in inline HTML attributes and jQuery handlers. In React you call e.preventDefault(), and e.stopPropagation() separately if you also want to stop bubbling.

open as a page

In React 19, what object does React pass to an event handler such as onClick, and how do you reach the underlying browser event from it?

level: juniorimportance: must knowfreq 65%

basics

~10 s

React passes a SyntheticEvent: a cross-browser wrapper that mirrors the DOM event interface (type, target, preventDefault, stopPropagation). The untouched browser event is always available on e.nativeEvent for anything React does not normalize.

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

In React 19, when you write <button onClick={handleClick}>, does React call addEventListener on that button DOM node? Explain what React does instead and how your handler ends up running.

level: juniorimportance: should knowfreq 45%

basics

~10 s

No. React registers listeners once on the root container passed to createRoot, then dispatches a click by finding the target's component and walking up the component tree, calling the onClick props it collects.

open as a page

A modal rendered through createPortal into document.body still triggers the onClick of the React component that rendered it, even though the modal's DOM node is not inside that component's DOM. Why does React dispatch it that way, and what does it break?

level: middleimportance: should knowfreq 50%

basics

~20 s

React propagates events along the component tree, not the DOM tree. It registers listeners on the portal's container too, then walks the portal content's React parent chain — which still includes the component that rendered it — so ancestors receive the event.

open as a page

React 17 moved React's delegated event listeners off document and onto the container element passed into the root API. Which real problems did that change fix, and what does it mean for a page running more than one React root?

level: middleimportance: should knowfreq 32%

basics

~20 s

Attaching at the root container makes React a normal participant in DOM propagation: stopPropagation in a React handler now really stops document-level listeners, non-React code between the root and document no longer silences React, and independent roots on one page stop interfering with each other.

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

What does React's onClickCapture prop do that onClick does not, and when would you reach for it?

level: middleimportance: should knowfreq 35%

basics

~20 s

onClickCapture registers the handler for the capture phase, so it runs on ancestors first, top-down, before the clicked element's own onClick. React offers a Capture variant of every event prop for intercepting an interaction before descendants handle it.

open as a page

Older React guidance says you must call e.persist() before using an event inside setTimeout or after an await. What was event pooling, and what is actually true from React 17 onward?

level: middleimportance: should knowfreq 45%

basics

~20 s

Before React 17, React reused one SyntheticEvent object per event type and nulled its fields once the handler returned, so async reads saw null unless you called e.persist(). React 17 removed pooling; in React 19 the event is an ordinary object and persist() does nothing.

open as a page

A React 19 component attaches its own click listener to a child DOM node with addEventListener via a ref, and the same subtree also has React onClick and onClickCapture props. In what order do these run, and what happens when either side calls stopPropagation?

level: seniorimportance: should knowfreq 42%

basics

~20 s

React dispatches from a listener on the root container, so a native bubble listener on an inner node runs before React's onClick, while React's onClickCapture runs before native capture listeners on descendants. A native stopPropagation below the root cancels React entirely.

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

You need a property that React's SyntheticEvent does not expose — for example the submitter of a form's submit event. How do you decide when to drop to e.nativeEvent, and what do you take on when you do?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

React normalizes a curated subset of each event's properties; anything outside it lives on e.nativeEvent, which is the browser's own object. Reading it is supported and normal, but you inherit raw browser behaviour, so you own the compatibility check.

open as a page