Problem Solved

React · pass function as prop

Home › Courses › React › Problems › Function as prop
⚛️ Problem · 5 min read

How to pass a function as a prop.
The callbacks pattern.

Children trigger, parents handle. That's the React data flow — props go down, callbacks come up. Passing a function as a prop is how the callback goes up. But there's one critical mistake everyone makes (calling it instead of passing it), and a decision between arrow functions and method references. This page covers both.

Short answer: Pass the function reference, not the result of calling it. onClick={handleClick} ✅. onClick={handleClick()} ❌ (calls it during render). The child receives the function and calls it whenever — on click, in an effect, in another handler. For arguments: wrap in an arrow — onClick={() => handleClick(id)}. No .bind(this) in function components.

✅ Solution 1 — Pass the function, not the call

The #1 mistake — happens to everyone once.

When you write onClick={handleClick()}, you're not passing handleClick — you're calling it immediately and passing its return value. For a function that returns nothing, the child receives undefined. For a function that returns a value, the child receives that value (usually not a function). Either way, it's broken. The fix: remove the parentheses.

pass-vs-call.tsx
import { useState } from 'react';

// ─── Child component ─────────────────────────
function Button({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
return ;
}

// ─── BAD: calling the function during render
function BadParent() {
const [count, setCount] = useState(0);

function handleClick() {
setCount(c => c + 1);
}

return (

{/* ❌ Calls handleClick during render.
→ count increments immediately on render
→ re-render → calls again → infinite loop or wrong behavior
→ Button receives undefined (handleClick returns void)
→ clicking does nothing */}
);
}

// ─── GOOD: passing the reference ─────────────
function GoodParent() {
const [count, setCount] = useState(0);

function handleClick() {
setCount(c => c + 1);
}

return (

{count}

{/* ✅ Passes the function. Button decides when to call. */}
); } // ─── Passing with arguments ────────────────── function ArgParent() { const [message, setMessage] = useState(''); function handleClick(text: string) { setMessage(text); } return (

{message}

{/* ❌ Wrong: calling with no args /} {/ */} {/* ✅ Correct: wrap in arrow to pass args */}
); } // ─── The rule ──────────────────────────────── // // onClick={handleClick} → pass the function ✅ // onClick={handleClick()} → call during render ❌ // onClick={() => handleClick()} → call on click ✅ // // Adding () = calling. Adding () => = wrapping. // Wrap when you need args or additional logic. console.log(Button, BadParent, GoodParent, ArgParent);
import { useState } from 'react';

function Button({ onClick, children }) {
return React.createElement('button', { onClick }, children);
}

function BadParent() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(c => c + 1);
}
return React.createElement(Button, { onClick: handleClick() }, 'Click');
}

function GoodParent() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(c => c + 1);
}
return React.createElement('div', null,
React.createElement('p', null, count),
React.createElement(Button, { onClick: handleClick }, 'Click')
);
}

function ArgParent() {
const [message, setMessage] = useState('');
function handleClick(text) {
setMessage(text);
}
return React.createElement('div', null,
React.createElement('p', null, message),
React.createElement(Button, { onClick: () => handleClick('Hello') }, 'Hello'),
React.createElement(Button, { onClick: () => handleClick('Hi') }, 'Hi')
);
}

console.log(Button, BadParent, GoodParent, ArgParent);

Reference vs call

onClick={fn} passes the function. onClick={fn()} calls it and passes the result. Different things entirely.

Adding () calls it

Every time you add () after a function name, you call it. In JSX, only add () inside an arrow function or callback.

Wrapping to pass args

onClick={() => fn('x')} — the arrow is called on click, which calls fn with the arg. The whole pattern in one line.

Common symptom

"My button does nothing" + "state updates on render" = you called the function in the prop. Check the props for stray ().

The mental model: {'{fn}'} passes, {'{fn()}'} calls. When you write JSX, everything inside {'{...}'} is a JavaScript expression. So {'{handleClick}'} evaluates to the function reference, and {'{handleClick()}'} evaluates to whatever handleClick returns. For a callback, you want the function to be called later, by the child. So you pass the reference — no (). If you find yourself debugging "my button doesn't work," check the JSX for stray () before anything else.

✅ Solution 2 — Method reference vs arrow function

Both work. Different trade-offs.

Two ways to pass a handler. Method reference: onClick={handleClick} — stable, memoizable. Arrow function: onClick={() => handleClick(id)} — new each render, needed for args. Neither is wrong. The choice depends on whether you need to pass arguments and whether the child is memoized.

reference-vs-arrow.tsx
import { useState, memo } from 'react';

// ─── Method reference — stable, memo-friendly
function RefParent() {
const [count, setCount] = useState(0);

function handleIncrement() {
setCount(c => c + 1);
}

return (

{count}

{/* handleIncrement is a new function each render (function declaration) BUT for a single handler, that's fine — React.memo can still help if you useCallback it */}
); } // ─── Arrow function — new each render ──────── const MemoChild = memo(({ onClick }: { onClick: () => void }) => { console.log('MemoChild rendered'); return ; }); function ArrowParent() { const [count, setCount] = useState(0); return (

{count}

{/* ❌ New function each render — MemoChild re-renders every time */} setCount(c => c + 1)} />
); } // ─── Passing args with arrow — the right way function ListWithArgs() { const [items, setItems] = useState([ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' } ]); function remove(id: string) { setItems(prev => prev.filter(i => i.id !== id)); } return (
    {items.map(item => (
  • {item.name} {/* ✅ Arrow passes the id, calls remove on click */}
  • ))}
); } // ─── Memoized handler + arrow — best of both import { useCallback } from 'react'; function BestParent() { const [count, setCount] = useState(0); // ✅ Stable — MemoChild skips re-render const handleIncrement = useCallback(() => { setCount(c => c + 1); }, []); return (

{count}

); } // ─── Decision matrix ───────────────────────── // // No args, no memo'd child: // onClick={handleClick} — simplest, fine // // No args, memo'd child: // const cb = useCallback(() => {...}, []); // onClick={cb} — stable, memo works // // Args, any case: // onClick={() => handle(id)} — needed for args // // Inline simple logic: // onClick={() => setCount(0)} — fine, no memo issue // // Each render creates a new arrow function. For non-memo'd // children, this is harmless. For memo'd children, it // defeats the memo — use useCallback or method reference. console.log(RefParent, MemoChild, ArrowParent, ListWithArgs, BestParent);
import { useState, memo, useCallback } from 'react';

function RefParent() {
const [count, setCount] = useState(0);
function handleIncrement() {
setCount(c => c + 1);
}
return React.createElement('div', null,
React.createElement('p', null, count),
React.createElement(MemoChild, { onClick: handleIncrement })
);
}

const MemoChild = memo(({ onClick }) => {
console.log('MemoChild rendered');
return React.createElement('button', { onClick }, 'Click');
});

function ArrowParent() {
const [count, setCount] = useState(0);
return React.createElement('div', null,
React.createElement('p', null, count),
React.createElement(MemoChild, { onClick: () => setCount(c => c + 1) })
);
}

function ListWithArgs() {
const [items, setItems] = useState([
{ id: '1', name: 'Alice' },
{ id: '2', name: 'Bob' }
]);
function remove(id) {
setItems(prev => prev.filter(i => i.id !== id));
}
return React.createElement('ul', null,
items.map(item =>
React.createElement('li', { key: item.id },
item.name,
React.createElement('button', { onClick: () => remove(item.id) }, '×')
)
)
);
}

function BestParent() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(() => {
setCount(c => c + 1);
}, []);
return React.createElement('div', null,
React.createElement('p', null, count),
React.createElement(MemoChild, { onClick: handleIncrement })
);
}

console.log(RefParent, MemoChild, ArrowParent, ListWithArgs, BestParent);

Method reference is stable

onClick={handleClick} — same function reference unless useCallback wraps it. Good for memo'd children.

Arrow is new each render

onClick={() => ...} — fresh function every render. Fine for non-memo'd children; defeats React.memo.

Arrow for args

onClick={() => remove(item.id)} — the standard way to pass arguments. There's no cleaner option.

useCallback when it matters

Only optimize with useCallback when the child is memo'd and the props would otherwise change every render.

✅ Solution 3 — Callback props with types

Typing callbacks properly in TypeScript.

In TypeScript, a callback prop has a function type. onClick: () => void, onChange: (value: string) => void, onSelect: (item: User) => void. The parent must pass a function with a matching signature. This is how React + TS combines props and callbacks safely.

typed-callbacks.tsx
import { useState } from 'react';

// ─── Simple callback props ───────────────────
interface ButtonProps {
onClick: () => void; // no args
onDoubleClick?: () => void; // optional
children: React.ReactNode;
}

function Button({ onClick, onDoubleClick, children }: ButtonProps) {
return (

);
}

// ─── Callback with args ──────────────────────
interface SearchBoxProps {
onSearch: (query: string) => void; // takes string
onClear: () => void;
}

function SearchBox({ onSearch, onClear }: SearchBoxProps) {
const [query, setQuery] = useState('');

function handleSubmit(e: React.FormEvent) {
e.preventDefault();
onSearch(query); // ✅ passes the query to the parent
}

function handleClear() {
setQuery('');
onClear();
}

return (
setQuery(e.target.value)} />
); } // ─── Parent with typed handlers ────────────── function Parent() { const [results, setResults] = useState<string[]>([]); function handleSearch(query: string) { fetch(/api/search?q=${query}`) .then(r => r.json()) .then(setResults); } function handleClear() { setResults([]); } return (
    {results.map(r =>
  • {r}
  • )}
); } // ─── Callback with an object argument ──────── interface User { id: string; name: string } interface UserListProps { users: User[]; onSelect: (user: User) => void; onDelete: (id: string) => void; } function UserList({ users, onSelect, onDelete }: UserListProps) { return (
    {users.map(user => (
  • ))}
); } // ─── TypeScript catches prop mismatches ────── function TypeSafety() { return (
{/* ❌ Compile errors: /} {/ console.log(q)} // q is inferred as string onClear={() => {}} />
); } // ─── The pattern ───────────────────────────── // // Child: define prop as function type // interface Props { onX: (arg: T) => void } // // Child: call the function // function Child({ onX }: Props) { // return
import { useState } from 'react';

function Button({ onClick, onDoubleClick, children }) {
return React.createElement('button', { onClick, onDoubleClick }, children);
}

function SearchBox({ onSearch, onClear }) {
const [query, setQuery] = useState('');
function handleSubmit(e) {
e.preventDefault();
onSearch(query);
}
function handleClear() {
setQuery('');
onClear();
}
return React.createElement('form', { onSubmit: handleSubmit },
React.createElement('input', { value: query, onChange: e => setQuery(e.target.value) }),
React.createElement('button', { type: 'submit' }, 'Search'),
React.createElement('button', { type: 'button', onClick: handleClear }, 'Clear')
);
}

function Parent() {
const [results, setResults] = useState([]);
function handleSearch(query) {
fetch(/api/search?q=${query}`).then(r => r.json()).then(setResults);
}
function handleClear() {
setResults([]);
}
return React.createElement('div', null,
React.createElement(SearchBox, { onSearch: handleSearch, onClear: handleClear }),
React.createElement('ul', null, results.map(r => React.createElement('li', { key: r }, r)))
);
}

function UserList({ users, onSelect, onDelete }) {
return React.createElement('ul', null,
users.map(user =>
React.createElement('li', { key: user.id },
React.createElement('button', { onClick: () => onSelect(user) }, user.name),
React.createElement('button', { onClick: () => onDelete(user.id) }, '×')
)
)
);
}

function TypeSafety() {
return React.createElement('div', null,
React.createElement(Button, { onClick: () => console.log('clicked') }, 'OK'),
React.createElement(SearchBox, {
onSearch: (q) => console.log(q),
onClear: () => {}
})
);
}

console.log(Button, SearchBox, Parent, UserList, TypeSafety);

Function type = callback

onClick: () => void — the type signature is a function. The parent must pass a function matching it.

Args type-check

onSearch: (query: string) => void — the parent's handler receives string. Wrong type = compile error.

Optional callbacks

onDoubleClick?: () => void — with ?, the parent can omit it. Child checks with ?.() or if (onDoubleClick).

The whole pattern

Child receives function → calls it with args → parent handles. Types ensure signature matches at every call site.

⚠️ Common mistakes

The function-as-prop mistakes that cost real time.

1. onClick={fn()}

Calls during render instead of passing. Fix: onClick={fn} or onClick={() => fn()}.

2. Passing undefined as a callback

— child crashes on click. Fix: optional prop with ?., or default to noop.

3. Using .bind(this) in a function component

No this in function components. Fix: drop the bind — function components don't need it.

4. onClick={() => fn}

Arrow returns the function without calling. Fix: onClick={fn} or onClick={() => fn()}.

5. Forgetting args in the arrow

onChange={() => setName} — doesn't pass event. Fix: onChange={(e) => setName(e.target.value)}.

6. Over-using useCallback

Wrapping every handler without a memo'd child. Fix: only memoize when the child is memo'd or the reference matters.

7. Type mismatch on the callback

onSearch={(n: number) => {}} when the prop expects (string) => void. Fix: match the signature.

8. Async callbacks that aren't handled

onClick={async () => fetch(...)} — errors swallowed. Fix: try/catch, or handle state updates for error.

❓ Frequently asked

How do I pass a function as a prop in React?

Pass it like any other prop, but don't call it: onClick={handleClick}, not onClick={handleClick()}. The first passes the function reference; the second calls it during render and passes the return value.

Should I use arrow functions or method references as props?

Both are valid. onClick={handleClick} passes a stable reference — better for React.memo'd children. onClick={() => handleClick(id)} creates a new function each render — needed when you must pass arguments.

Why do I get 'handleClick is not a function' when I pass a prop?

Either you passed the result of calling the function (onClick={handleClick()}) instead of the function itself (onClick={handleClick}), or the child isn't receiving the prop. Check the parent's prop name matches the child's destructuring.

Do I need to bind 'this' in functional components?

No — that's a class component issue. Functional components don't have 'this'. Every function you define inside a function component closes over the surrounding variables automatically.

🔗 Related

← All React problems
Problem solved · React