Skip to main content

Passing Event Handlers as Props in React

Passing event handlers as props is the foundational pattern for creating reusable React components. By sending a function from a parent component to a child, you decouple presentation logic (how a button looks) from behavior logic (what happens when it's clicked). This allows you to build generic, composable components like buttons, inputs, and modals that work across your entire application with different behaviors.

Understanding the Core Concept: Separation of Concerns

Why pass event handlers as props instead of defining logic in child components?

In React, components should have clear, single responsibilities. When you embed event handler logic directly inside a child component, you make that component inflexible and reusable only for one specific task.

Consider this inflexible approach:

// Bad: Button is tied to one specific action
function PlayButton() {
function handleClick() {
alert('Playing movie!');
}
return <button onClick={handleClick}>Play Movie</button>;
}

This PlayButton can only play movies. If you need a button that uploads images or saves a form, you'd have to create entirely new button components—code duplication at scale.

Instead, separate concerns:

  • Parent component defines what should happen (the business logic and specific action).
  • Child component defines when it should happen (it detects a click and calls the handler).

This separation is called decoupling, and it's the secret to reusable components. The parent passes a function (event handler) to the child via props, allowing one generic Button component to power unlimited use cases.

Building a Reusable Button Component

Create a generic Button that accepts an onClick prop

Start with a simple, reusable Button component:

function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}

export default Button;

This component is "behavior-agnostic"—it has no idea what the onClick handler does. It simply receives a function via the onClick prop, wires it to the HTML button's onClick event, and calls it when clicked. The children prop allows you to customize the button's label.

Use the Button in a parent component with different behaviors

Now create a parent component that uses the Button multiple times, each with different handlers:

function Toolbar() {
function handlePlayClick() {
alert('Playing movie!');
}

function handleUploadClick() {
alert('Uploading image!');
}

return (
<div>
<Button onClick={handlePlayClick}>
Play Movie
</Button>

<Button onClick={handleUploadClick}>
Upload Image
</Button>

<Button onClick={() => alert('Saving...')}>
Save
</Button>
</div>
);
}

The Toolbar component defines three handlers with different logic and passes each one to a Button instance. Each button looks and behaves identically from a UI perspective, but performs different actions. This is the power of decoupling: one reusable component, infinite behaviors.

How Event Handler Props Work

Step-by-step: how a function flows from parent to child and triggers

  1. Parent defines the handlerhandlePlayClick is defined in the Toolbar component's scope.
  2. Parent passes it as a prop<Button onClick={handlePlayClick}> sends the function reference down.
  3. Child receives it — The Button component receives onClick in its props object.
  4. Child attaches it to an event — The Button wires it to the JSX element: <button onClick={onClick}>.
  5. When the event fires, the handler runs — User clicks the button, the HTML <button> element's onClick fires, React calls the handler function, and the parent's logic executes.

Critically, the child doesn't care what the handler does; it only knows when to call it. This is decoupling in action.

Passing handlers inline vs. as named functions

You can pass handlers two ways:

// Option 1: Named function (clearer for complex logic)
function Toolbar() {
function handleSave() {
// Complex save logic here
}
return <Button onClick={handleSave}>Save</Button>;
}

// Option 2: Inline arrow function (simpler for small operations)
function Toolbar() {
return <Button onClick={() => alert('Saved!')}>Save</Button>;
}

// Option 3: Inline arrow function with parameters
function Toolbar() {
return (
<Button onClick={(id) => deleteItem(id)}>
Delete
</Button>
);
}

Use named functions for complex logic; use inline functions for simple, one-line operations. Avoid inline functions in performance-critical lists (they recreate on every render), and use useCallback to memoize handlers when necessary.

Naming Conventions for Event Handler Props

How should you name props that receive event handlers?

The built-in HTML elements use prop names like onClick, onChange, onSubmit. However, when designing your own components, you can name event handler props more semantically to describe the action rather than the event.

Compare these approaches:

// Less descriptive: generic "onAction"
<Toolbar onAction={() => doSomething()} />

// Better: semantic, action-specific names
<Toolbar
onPlayMovie={() => play()}
onUploadImage={() => upload()}
onSaveForm={() => save()}
/>

Naming convention for event handler props:

  • Start with on (e.g., onPlayMovie, onUploadImage).
  • Follow with a capital letter (camelCase).
  • Describe the action from the user's perspective, not the HTML event.

Examples:

Good prop nameWhat it means
onSubmitUser submitted a form
onDeleteUser clicked delete
onToggleUser toggled something
onSearchUser searched for something
onFilterChangeUser changed a filter

This naming makes your component's contract clear to other developers. When they see <UserCard onDelete={handleUserDelete}>, they immediately understand that the card will call handleUserDelete when the user performs a delete action.

Layering handlers across multiple components

Handlers can flow through multiple component levels:

// App.jsx
function App() {
function handleDeleteUser(userId) {
// Actual delete logic here
}

return <UserList onDeleteUser={handleDeleteUser} />;
}

// UserList.jsx
function UserList({ onDeleteUser }) {
const users = [...];
return (
<div>
{users.map(user => (
<UserCard
key={user.id}
user={user}
onDelete={() => onDeleteUser(user.id)}
/>
))}
</div>
);
}

// UserCard.jsx
function UserCard({ user, onDelete }) {
return (
<div>
<p>{user.name}</p>
<button onClick={onDelete}>Delete</button>
</div>
);
}

Here, the delete handler originates in App, flows through UserList, and is finally attached to the button in UserCard. Each layer adds specificity: App knows how to delete from the database, UserList knows which user to delete (and passes the userId), and UserCard knows when to trigger the deletion.

Best Practices for Event Handler Props

Guidelines for writing maintainable, reusable components

  • Name props semantically — Use onDelete, onSave, onSearch; avoid generic onAction or onClick for custom components.
  • Define handler functions outside JSX for readability — For logic beyond one line, extract handlers to named functions above the return statement.
  • Document expected prop signatures — In comments or TypeScript types, clarify what parameters your handler expects: onDelete(userId: string).
  • Provide default handlers — Prevent errors from missing handlers by providing sensible defaults or checking if the handler exists before calling.
  • Avoid handler recreation on every render — Use useCallback in parent components if the handler is passed to many children (prevents unnecessary re-renders in performance-sensitive lists).
  • Use TypeScript for prop validation — Type your event handler props to catch errors early: onClick: (e: React.MouseEvent<HTMLButtonElement>) => void.

Example with defaults and TypeScript:

interface ButtonProps {
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
}

function Button({ onClick = () => {}, children }: ButtonProps) {
return (
<button onClick={onClick}>
{children}
</button>
);
}

Key Takeaways

  • Functions are data — Event handlers are functions that can be passed as props, just like strings or numbers.
  • Decoupling enables reusability — Separating what a component does from when it does it allows one component to serve unlimited purposes.
  • Parent defines the "what" — Parent components specify the business logic and action to perform.
  • Child defines the "when" — Child components detect events and call the handler at the appropriate moment.
  • Naming conventions clarify intent — Use semantic names like onDelete and onSubmit to make your component's expected behavior clear.
  • This pattern scales — It's the foundation of composable React applications with hundreds of reusable components.

Frequently Asked Questions

What's the difference between passing a function and calling it immediately?

Passing a function: onClick={handleClick} passes the function reference. React calls it when the event fires. Calling immediately: onClick={handleClick()} calls the function right now during render, executing it once and passing its return value (usually undefined) to onClick. Always pass the function reference, not the result of calling it.

Can I pass data to an event handler?

Yes, using arrow functions or .bind(). Wrap the handler in an arrow function that calls the real handler with parameters: onClick={() => deleteItem(itemId)} or onClick={deleteItem.bind(this, itemId)}. Arrow functions are cleaner for React components.

Should I use onClick or a semantic name like onDelete for custom components?

Use semantic names for custom components (onDelete, onSave) because they describe the action from the user's perspective. Reserve onClick for components that directly wrap an HTML <button> element. This makes your API clearer and more self-documenting.

How do I pass multiple parameters to an event handler?

Use an arrow function that accepts the event and calls your handler with additional parameters: onClick={(e) => handleDelete(itemId, e)}. Or create a closure: onClick={() => handleDelete(itemId)}. Arrow functions are simpler.

What's the performance cost of passing handlers as props?

Minimal for typical applications. The performance cost comes from unnecessary re-renders when a handler is recreated on every render. Optimize using useCallback in performance-critical scenarios like large lists, but for most UI components, passing handlers is cheap. Clarity matters more than this micro-optimization.

Further Reading