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 useMemo or useCallback dependencies

  • 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.