Why onClick={() => doThing()} Is Different from onClick={doThing}
React Version
February 17, 2026

Reviewed a pull request recently for a local project/demo. One of my friends who is also a contributor to that demo changed all the onClick={doThing} to
onClick={() => doThing()}.
Asked why. They said “that’s how the tutorial did it.”
These aren’t the same thing. And understanding the difference matters more than you’d think.
The Thing Everyone Does
Look at any React tutorial. You’ll see both patterns:
// Pattern 1\n<button onClick={handleClick}>Click me</button>\n\n// Pattern 2\n<button onClick={() => handleClick()}>Click me</button>
Both work. Button clicks, function runs. What’s the difference?
Took me longer than I want to admit to understand this.
What’s Actually Different
Pattern 1: You’re passing a reference to the function.
<button onClick={handleClick}>
React gets the function handleClick. When the button is clicked, React calls that function.
Pattern 2: You’re creating a new function every render.
<button onClick={() => handleClick()}>
Every time this component renders, JavaScript creates a brand new arrow function. That arrow function calls handleClick when executed.
Same behavior from the user’s perspective. Completely different from React’s perspective.
The Reference Equality Thing
JavaScript compares functions by reference, not by what they do.
function sayHi() {\n console.log('hi');\n}\n\nconst a = sayHi;\nconst b = sayHi;\n\nconsole.log(a === b); // true - same reference
But arrow functions created inline are new every time:
const a = () => console.log('hi');\nconst b = () => console.log('hi');\n\nconsole.log(a === b); // false - different references
Even though they do the exact same thing, they’re not equal.
This matters in React.
When This Actually Matters
Built a component with a memoized child:
const ExpensiveChild = React.memo(({ onClick }) => {\n console.log('Rendering ExpensiveChild');\n return <button onClick={onClick}>Click</button>;\n});\n\nfunction Parent() {\n const [count, setCount] = useState(0);\n\n const handleClick = () => {\n console.log('Clicked');\n };\n\n return (\n <div>\n <button onClick={() => setCount(count + 1)}>\n Increment: {count}\n </button>\n <ExpensiveChild onClick={() => handleClick()} />\n </div>\n );\n}
Expected: ExpensiveChild only re-renders when its props change.
Reality: It re-renders every time count changes.
Why? The arrow function () => handleClick() is a new function on every render. React.memo sees a different onClick prop each time. Thinks the props changed. Re-renders the child.
This breaks memoization.
The Fix
Pass the function reference directly:
<ExpensiveChild onClick={handleClick} />
Now handleClick is the same reference every render. React.memo sees identical props. Skips the re-render.
Or use useCallback:
const handleClick = useCallback(() => {\n console.log('Clicked');\n}, []);\n\nreturn <ExpensiveChild onClick={handleClick} />;
useCallback memoizes the function. Returns the same reference across renders (unless dependencies change).
When You Need The Arrow Function
Sometimes you need to pass arguments:
{items.map(item => (\n <button key={item.id} onClick={() => handleClick(item.id)}>\n {item.name}\n </button>\n))}
Can’t do onClick={handleClick(item.id)} - that calls the function immediately during render.
Can’t do onClick={handleClick} - the function doesn’t know which item was clicked.
Need the arrow function to capture item.id.
Alternative using bind:
<button onClick={handleClick.bind(null, item.id)}>
But arrow functions are clearer.
Or pass the ID as a data attribute:
<button data-id={item.id} onClick={handleClick}>\n\nfunction handleClick(e) {\n const id = e.currentTarget.dataset.id;\n // use id\n}
More verbose. Sometimes cleaner. Depends on the situation.
The Performance Thing Everyone Worries About
“Creating functions on every render is slow!”
Is it though?
Ran a test. Component with 1000 buttons. Each with an inline arrow function.
{Array(1000).fill(0).map((_, i) => (\n <button key={i} onClick={() => handleClick(i)}>\n Button {i}\n </button>\n))}
Re-rendered the component repeatedly. Profiled it.
Difference between arrow functions and direct references? Negligible. Like, barely measurable.
Function creation is cheap. Really cheap. Modern JavaScript engines are fast at this.
The performance issue isn’t the function creation. It’s the unnecessary re-renders of child components when you break memoization.
The Real Problem
Not the arrow function itself. It’s that it breaks React.memo, useMemo, and useCallback optimizations.
const MemoizedList = React.memo(({ onItemClick }) => {\n // Expensive rendering\n return <div>{/* lots of items */}</div>;\n});\n\n// Bad - re-renders MemoizedList every time\n<MemoizedList onItemClick={() => handleItemClick()} />\n\n// Good - only re-renders when handleItemClick reference changes\n<MemoizedList onItemClick={handleItemClick} />
If you’re not using memoization, arrow functions are fine.
If you are using memoization, arrow functions break it.
When I Actually Use Each
Direct reference (onClick={handleClick}):
-
No arguments needed
-
Passing to memoized components
-
The default pattern I use
Arrow function (onClick={() => handleClick()}):
-
Need to pass arguments
-
Need to call multiple functions
-
Component isn’t memoized anyway
useCallback:
-
Passing functions to memoized components
-
Function has dependencies that change
-
Actually measuring performance issues
The Event Handler Pattern
Common pattern I use:
function handleClick(id) {\n return (e) => {\n // e is the event\n // id is captured from closure\n doSomething(id);\n };\n}\n\nreturn (\n <button onClick={handleClick(item.id)}>\n Click\n </button>\n);
Creates a function that returns a function. Captures the ID in a closure. Clean syntax.
But creates a new function every render. Same issue as arrow functions.
For non-memoized components? Fine. For memoized? Problem.
The ESLint Rule
There’s an ESLint rule about this: react/jsx-no-bind.
It warns when you use arrow functions or .bind() in JSX.
I don’t use this rule. Too strict.
Arrow functions in JSX are often fine. Only matters when you’re optimizing with memoization.
Don’t prematurely optimize. Use arrow functions when they make sense. Optimize when you have actual performance problems.
What Actually Matters
-
For most components: Arrow functions are fine. The performance impact is negligible.
-
For lists or frequently updating components: Be more careful. Arrow functions can cause unnecessary re-renders.
-
For memoized components: Pass stable references or use
useCallback.
Don’t cargo-cult patterns. Understand when each approach matters.
The DevTools Check
Want to see if your component is re-rendering unnecessarily?
React DevTools has a “Highlight updates when components render” option.
Turn it on. Interact with your app. See what flashes.
If memoized components are flashing when they shouldn’t be, check your props. Probably passing a new arrow function or object each render.
The Actual Performance Bottleneck
It’s almost never the arrow function creation.
It’s the re-renders. The DOM updates. The expensive computation inside components.
Creating a function? Microseconds.
Re-rendering a large list? Milliseconds.
Focus on the real bottleneck.
When I Learned This
Built a data table. Thousands of rows. Each row had action buttons. Used arrow functions everywhere.
Scrolling was janky. Thought “oh, it’s the arrow functions.”
Profiled it. Function creation wasn’t the problem. Re-rendering all rows on every state change was the problem.
Memoized the row component. Passed stable function references instead of arrow functions. Suddenly smooth.
Wasn’t the arrow functions themselves. It was breaking memoization.
The Mental Model
Arrow functions create new functions every render.
That’s fine if:
-
Component isn’t memoized
-
Not passing to memoized children
-
Not causing unnecessary re-renders
That’s a problem if:
-
Breaking
React.memo -
Breaking
useMemooruseCallbackdependencies -
Causing measurable performance issues
Most of the time, it’s fine.
What I Actually Do
Default to direct references when possible:
<button onClick={handleClick}>
Use arrow functions when needed:
<button onClick={() => handleClick(item.id)}>
Add useCallback if passing to memoized components:
const handleClick = useCallback(() => {\n doThing();\n}, []);
Don’t overthink it until there’s an actual performance problem.
Thanks for reading Under The Hood! Subscribe for free to receive new posts and support my work.