How to Handle Events in React

Beginner10 min interview
Skills tested:
Attaching event handlers as JSX props using camelCase event namesPassing a function reference rather than calling the function directly in JSXUsing e.preventDefault() and e.stopPropagation() correctlyPassing extra data to event handlers using closures or arrow functionsUnderstanding React event delegation and how it differs from native addEventListener

Advertisement

🧩 Scenario

Event handling is the foundation of interactivity in React. Every button click, input change, form submit, key press, and drag interaction is an event. Understanding synthetic events, handler patterns, and common gotchas is expected in any React interview.

Architecture Walkthrough

Synthetic Events and Event Delegation

React does not attach event listeners to individual DOM nodes. It uses event delegation: a single listener at the root captures all events via bubbling. React intercepts the native event and wraps it in a SyntheticEvent object that normalizes the event interface across browsers. The SyntheticEvent has the same API as the native event (type, target, preventDefault, stopPropagation, etc.) but is consistent across environments.

In React 17, the root changed from document to the React root DOM node, which allows multiple React versions on the same page to have independent event systems.

Passing References vs Calling Functions

The most common beginner mistake: onClick={handleClick()} calls handleClick immediately during render and assigns its return value (usually undefined) as the handler. The button becomes unresponsive. onClick={handleClick} passes the function reference; React calls it when the click occurs.

For handlers that need extra data (like an item ID in a list), wrap with an arrow function: onClick={() => handleDelete(item.id)}. This creates a new function reference on each render. For memoized children, wrap the base handler in useCallback and use a data attribute or closure to pass the ID.

preventDefault and stopPropagation

e.preventDefault() prevents the browser's default action for the event: form submission page reload, link navigation, context menu on right-click. It does not affect event propagation.

e.stopPropagation() prevents the event from bubbling further up the DOM tree. Parent elements with the same event type will not receive it. It does not affect the browser's default action.


Key Code Explained

// Basic event handling: pass reference, not call
function SearchForm({ onSearch }: { onSearch: (query: string) => void }) {
  const [query, setQuery] = useState('');

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault(); // prevent page reload on form submit
    if (query.trim()) {
      onSearch(query.trim());
    }
  };

  return (
    <form onSubmit={handleSubmit}> {/* pass reference, not handleSubmit() */}
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)} // arrow function to extract value
        placeholder="Search..."
      />
      <button type="submit">Search</button>
    </form>
  );
}


// Passing extra data to handlers in a list
interface Item {
  id: string;
  name: string;
}

function ItemList({ items, onDelete }: { items: Item[]; onDelete: (id: string) => void }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}

          {/* Option 1: arrow function wrapper — new reference each render */}
          <button onClick={() => onDelete(item.id)}>Delete</button>

          {/* Option 2: data attribute — avoids new reference, handler reads from dataset */}
          <button data-id={item.id} onClick={handleDeleteByDataset}>
            Delete (data attr)
          </button>
        </li>
      ))}
    </ul>
  );
}

function handleDeleteByDataset(e: React.MouseEvent<HTMLButtonElement>) {
  const id = e.currentTarget.dataset.id; // read from data attribute
  if (id) deleteItem(id);
}


// stopPropagation vs preventDefault
function ClickableCard({ children, onClick }: { children: React.ReactNode; onClick: () => void }) {
  return (
    <div onClick={onClick} className="card">
      {children}
      <a
        href="/details"
        onClick={(e) => {
          e.stopPropagation(); // prevents card's onClick from firing
          e.preventDefault();  // prevents navigation to /details
          openModal();
        }}
      >
        View Details
      </a>
    </div>
  );
}


// useCallback for stable handler in memoized child
const MemoizedDeleteButton = React.memo(({ itemId, onDelete }: { itemId: string; onDelete: (id: string) => void }) => {
  return <button onClick={() => onDelete(itemId)}>Delete</button>;
});

function ParentList({ items }: { items: Item[] }) {
  const [list, setList] = useState(items);

  // Stable reference: useCallback prevents MemoizedDeleteButton from re-rendering
  const handleDelete = useCallback((id: string) => {
    setList((prev) => prev.filter((item) => item.id !== id));
  }, []);

  return (
    <ul>
      {list.map((item) => (
        <li key={item.id}>
          {item.name}
          <MemoizedDeleteButton itemId={item.id} onDelete={handleDelete} />
        </li>
      ))}
    </ul>
  );
}

The e.currentTarget in handleDeleteByDataset is the element the handler is attached to (the button), while e.target is the element that was actually clicked. If the button contains a child span or icon, e.target could be the icon, but e.currentTarget is always the button. Use currentTarget when reading data attributes to avoid bugs from click targets being child elements.


Tradeoffs

Handler patternCreates new ref each renderWorks with React.memoUse when
Inline arrow functionYesNo (breaks memo)Simple cases, non-memoized children
Named function referenceNoYesTop-level handlers with no extra args
useCallbackNo (after first render)YesHandlers passed to memoized children
data attributeNoYesEvent delegation in a list

What Interviewers Actually Check

  • Whether you know to pass a reference (onClick={fn}) not a call (onClick={fn()})
  • Whether you call e.preventDefault() on form submit and link click handlers
  • Whether you know the difference between preventDefault and stopPropagation
  • Whether you know e.target vs e.currentTarget
  • Whether you know about React's synthetic event and event delegation model

Follow-Up Questions

  1. How does React's event delegation change when multiple React roots are on the same page (as allowed in React 17+)?
  2. What is passive event listeners and how does React's synthetic event system interact with them for scroll and touchstart?
  3. How would you implement a global keyboard shortcut handler using useEffect and addEventListener instead of JSX event props?
  4. How does React handle events that do not bubble (like focus and blur) and what are the onFocus / onBlur equivalents that do bubble?
  5. How would you use useTransition to mark a state update triggered by an event as low-priority to keep the input responsive?

Common Candidate Mistakes

  • Writing onClick={handleClick()} which calls the function during render instead of passing it as a handler
  • Forgetting e.preventDefault() on form submit, causing the page to reload and state to be lost
  • Using stopPropagation when preventDefault is needed (or vice versa), not understanding the difference
  • Assuming e.target is always the element the handler is on when it can be a child element
  • Not knowing that React event delegation in React 17+ attaches to the React root, not document

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you attach onClick, onChange, and onSubmit handlers correctly in JSX?
  • Can you explain the difference between onClick={handleClick} and onClick={handleClick()}?
  • Can you call e.preventDefault() to prevent default browser behavior?
  • Can you pass extra data to a handler in a list using an arrow function wrapper or data attributes?
  • Can you explain what React event delegation is and how it differs from attaching native listeners?

Summary

React handles events through a synthetic event system that wraps native browser events. Instead of attaching individual listeners to DOM nodes, React uses event delegation: one listener at the React root captures all events. The SyntheticEvent object provides a consistent interface across browsers.

Event handlers in JSX use camelCase names and receive a function reference, not a function call. onClick={handleClick} is correct; onClick={handleClick()} calls the function during render. For handlers that need extra context (like an item ID), wrap in an arrow function: onClick={() => handleDelete(item.id)}. For memoized children, use useCallback to stabilize the handler reference.

e.preventDefault() prevents the browser's default behavior for the event (form submit page reload, link navigation) without affecting event bubbling. e.stopPropagation() prevents the event from bubbling to parent elements without affecting the browser's default behavior. They are independent and can be used together.

Frequently Asked Questions

Is React SyntheticEvent the same as a native DOM event?

No. SyntheticEvent is a cross-browser wrapper that normalizes the event interface. In React 17+, it is no longer pooled, so properties like e.target.value remain accessible in async callbacks without calling e.persist().

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement