ReactPassing Event Handlers as Props

Passing Event Handlers as Props

In React, data flows down through props and events flow up through handler functions. When a child component needs to notify its parent that something happened — a button was clicked, a form was submitted, a value changed — the parent passes an event handler down as a prop and the child calls it at the right moment.

This pattern, often called "lifting state up," is fundamental to how React applications are structured. Understanding it clearly will make your component designs far more natural.

The Naming Convention

React has a widely-adopted naming convention for event-related things:

  • Prop names — use on prefix followed by the event in PascalCase: onClick, onChange, onSubmit, onDelete, onUserSelect

  • Handler function names — use handle prefix followed by what happened: handleClick, handleChange, handleSubmit, handleDelete, handleUserSelect

JSX
// Parent names the prop "onDelete" (describes the event)
// Parent names the function "handleDelete" (describes the action)
function TaskList() {
  function handleDelete(id) {
    setTasks(prev => prev.filter(task => task.id !== id))
  }

  return <Task onDelete={handleDelete} />
}

// Child receives "onDelete" as a prop and calls it
function Task({ task, onDelete }) {
  return (
    <div>
      <span>{task.title}</span>
      <button onClick={() => onDelete(task.id)}>Delete</button>
    </div>
  )
}
A Complete Parent-Child Example

Here's a Button component that accepts an onClick prop. The parent controls what happens when the button is clicked — the button itself is dumb and reusable:

JSX
// Reusable Button component — knows nothing about business logic
function Button({ onClick, children, disabled = false }) {
  return (
    <button onClick={onClick} disabled={disabled}>
      {children}
    </button>
  )
}

// Parent decides what happens on click
function PaymentForm() {
  const [processing, setProcessing] = useState(false)

  async function handlePayClick() {
    setProcessing(true)
    await processPayment()
    setProcessing(false)
  }

  return (
    <div>
      <Button onClick={handlePayClick} disabled={processing}>
        {processing ? 'Processing...' : 'Pay Now'}
      </Button>
    </div>
  )
}
Passing Arguments to Handlers

Sometimes the child needs to pass data back to the parent along with the event. The two common approaches are an inline arrow function and a curried handler:

JSX
// Approach 1: inline arrow function in JSX (most common)
function ColorPicker() {
  function handleColorSelect(color) {
    setSelectedColor(color)
  }

  const colors = ['red', 'green', 'blue', 'yellow']

  return (
    <div>
      {colors.map(color => (
        <button
          key={color}
          onClick={() => handleColorSelect(color)}
          style={{ background: color }}
        />
      ))}
    </div>
  )
}

// Approach 2: pass the handler directly when the child knows the value
function ColorButton({ color, onSelect }) {
  // Child can call onSelect directly if it knows what to pass
  return (
    <button onClick={() => onSelect(color)} style={{ background: color }}>
      {color}
    </button>
  )
}
Handlers Passed Directly vs Wrapped in Arrows

When no arguments need to be passed, you can give the handler directly without wrapping it in an arrow function:

JSX
function Toggle({ onToggle }) {
  // These are equivalent when no arguments are needed:
  return (
    <>
      {/* Direct reference — cleaner, no new function created */}
      <button onClick={onToggle}>Toggle</button>

      {/* Wrapped — only needed if you need to do something extra */}
      <button onClick={() => onToggle()}>Toggle</button>

      {/* With extra logic */}
      <button onClick={() => {
        console.log('toggling')
        onToggle()
      }}>Toggle</button>
    </>
  )
}
Note
Passing `onClick={onToggle}` and `onClick={() => onToggle()}` are functionally equivalent here. The direct reference is slightly more efficient since it doesn't allocate a new arrow function on each render — but the difference is negligible for most apps.
The Child Raises Events, the Parent Handles Them

A well-designed child component raises events when the user does something and leaves the decision of what to do entirely to the parent. This separation of concerns makes components maximally reusable.

JSX
// ✓ Good: SearchBar raises "onSearch", parent decides what to do with it
function SearchBar({ onSearch }) {
  const [query, setQuery] = useState('')

  function handleSubmit(e) {
    e.preventDefault()
    onSearch(query) // let the parent decide what happens
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <button type="submit">Search</button>
    </form>
  )
}

// Parent A: searches products
function ProductPage() {
  function handleSearch(query) {
    fetchProducts({ search: query })
  }
  return <SearchBar onSearch={handleSearch} />
}

// Parent B: searches users — same SearchBar, different behaviour
function AdminPage() {
  function handleSearch(query) {
    fetchUsers({ name: query })
  }
  return <SearchBar onSearch={handleSearch} />
}
Multiple Handlers on One Component

A component can accept multiple event handler props. Name each one after the specific thing the user did:

JSX
function FileCard({ file, onRename, onDelete, onDownload }) {
  return (
    <div className="file-card">
      <span>{file.name}</span>
      <div className="actions">
        <button onClick={() => onRename(file.id)}>Rename</button>
        <button onClick={() => onDownload(file.id)}>Download</button>
        <button onClick={() => onDelete(file.id)} className="danger">
          Delete
        </button>
      </div>
    </div>
  )
}

// Usage
function FileManager() {
  function handleRename(id) { /* open rename dialog */ }
  function handleDelete(id) { /* confirm and delete */ }
  function handleDownload(id) { /* trigger download */ }

  return files.map(file => (
    <FileCard
      key={file.id}
      file={file}
      onRename={handleRename}
      onDelete={handleDelete}
      onDownload={handleDownload}
    />
  ))
}
Arrow Functions in JSX: Performance Note

You'll often see discussions about whether inline arrow functions in JSX cause performance problems. The short answer: it rarely matters.

  • Inline arrows create a new function object on each render — this is usually negligible

  • The real issue is if the function is passed as a prop to a React.memo-wrapped child — a new function reference breaks memoization

  • In that specific case, use useCallback to stabilize the reference

  • For most components, write the cleaner code and only optimize when profiling shows a real problem

Warning
Don't prematurely optimize every event handler with `useCallback`. The overhead of `useCallback` itself (memoization bookkeeping, dependency comparison) can exceed the cost of the re-render you're trying to prevent. Profile first, optimize second.
Tip
If you're building a design system or component library, be more conservative about inline arrows — consumers of your components may wrap them in `React.memo` and expect stable prop references.