Lesson 44 of 80
Inline arrow vs. a curried handler factory — the real tradeoff is readability, not the performance cost people assume it is.
11 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
An event handler like onClick={handleClick} takes no arguments beyond the event React gives it for free. The moment you need to pass your own argument — which item was clicked, which row, which id — you need one more level of function. There are two common ways to get there: wrap it in an inline arrow, or write a function that takes your argument and returns a handler.
// Inline arrow — most common, most readable at the call site
<button onClick={() => deleteItem(item.id)}>Delete</button>
// Curried handler factory — a function that returns a function
function makeDeleteHandler(id) {
return () => deleteItem(id);
}
<button onClick={makeDeleteHandler(item.id)}>Delete</button>
Both do the same thing: produce a new function, scoped to this specific item.id, that React calls with no arguments of its own when the click fires. The inline arrow is shorter and keeps the argument visible right next to the JSX that uses it. The factory pulls the function-creation logic out of the render, which can help when the handler itself is nontrivial enough that inlining it clutters the markup.
function TodoList({ todos, onToggle }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
{todo.label}
</li>
))}
</ul>
);
}
Every render of this list creates a brand-new arrow function for every <input> — todos.length new functions, every time. That sounds expensive stated like that. In practice it almost never is: creating a function is one of the cheapest operations a JS engine does, and unless a handler is doing real, measurable work in React.memo's comparison (see this track's re-render lesson for the actual list of what forces a re-render in the first place), a fresh function reference per render is not what's making anything slow.
onClick handlers on plain DOM elements like <button> or <input> are never compared to anything, so a fresh closure each render costs nothing observable.makeDeleteHandler(item.id) still allocates a new closure on every call, same as the inline arrow — the factory changes where the function is written, not whether a new one gets created.This track's lesson on what actually forces a component to re-render draws a hard line between "a new value was created" and "something re-rendered because of it" — a prop changing only matters if a parent re-renders and something downstream is specifically comparing that prop by reference. Creating a new event-handler function on every render is a concrete instance of exactly that line: harmless by default, and only worth optimizing once something is actually comparing the function itself.