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.
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 (); } // ─── Passing with arguments ────────────────── function ArgParent() { const [message, setMessage] = useState(''); function handleClick(text: string) { setMessage(text); } return ({count}
{/* ✅ Passes the function. Button decides when to call. */}); } // ─── 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);{message}
{/* ❌ Wrong: calling with no args /} {/ */} {/* ✅ Correct: wrap in arrow to pass args */}
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.
import { useState, memo } from 'react'; // ─── Method reference — stable, memo-friendly function RefParent() { const [count, setCount] = useState(0); function handleIncrement() { setCount(c => c + 1); } return (); } // ─── 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}
{/* 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 */} ); } // ─── 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 ({count}
{/* ❌ New function each render — MemoChild re-renders every time */}setCount(c => c + 1)} />
-
{items.map(item => (
- {item.name} {/* ✅ Arrow passes the id, calls remove on click */} ))}
{count}
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.
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 (); } // ─── 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} )}
-
{users.map(user => (
- ))}
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.
onClick={fn()}Calls during render instead of passing. Fix: onClick={fn} or onClick={() => fn()}.
undefined as a callback — child crashes on click. Fix: optional prop with ?., or default to noop.
.bind(this) in a function componentNo this in function components. Fix: drop the bind — function components don't need it.
onClick={() => fn}Arrow returns the function without calling. Fix: onClick={fn} or onClick={() => fn()}.
onChange={() => setName} — doesn't pass event. Fix: onChange={(e) => setName(e.target.value)}.
Wrapping every handler without a memo'd child. Fix: only memoize when the child is memo'd or the reference matters.
onSearch={(n: number) => {}} when the prop expects (string) => void. Fix: match the signature.
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.