Event Handling in React

· 7 min read · Updated March 27, 2026 · intermediate
react events onClick onChange forms javascript

How React Events Differ from Vanilla JavaScript

If you’re coming from vanilla JavaScript, React’s event system feels familiar but has some key differences. React doesn’t attach event listeners directly to the elements you create — instead, it uses a single root-level listener and delegates all events. This is called the Synthetic Event System.

Here’s the most immediate difference in naming:

// Vanilla JS — lowercase, string handler
<button onclick="handleClick()">Click</button>

// React — camelCase, function reference
<button onClick={handleClick}>Click</button>

In vanilla JS, onclick accepts a string that gets evaluated, or a function reference via addEventListener. In React, you pass the function object directly as a JSX prop. No quotes, no addEventListener call needed.

Another critical difference is onChange. In the browser, a traditional onchange event only fires when an input loses focus after its value changed. In React, onChange fires on every keystroke — the same behavior as the browser’s oninput event. If you expect vanilla JS behavior from React’s onChange, you’ll be surprised.

React also normalizes events across browsers. Chrome, Firefox, Safari, and Edge all have subtle inconsistencies in how they expose events. React’s synthetic events give you a consistent API regardless of which browser your app runs in.

Handling Button Clicks with onClick

The onClick handler is the most common event you’ll use. You pass it a function that React calls when the element is clicked.

function ButtonClick() {
  function handleClick() {
    console.log('Button clicked at', new Date().toISOString());
    // output: Button clicked at 2026-03-27T10:30:00.000Z
  }

  return <button onClick={handleClick}>Click me</button>;
}

You can also define the handler inline if it’s short:

function InlineButton() {
  return (
    <button onClick={() => console.log('Clicked!')}>
      Click
    </button>
  );
}

Be careful not to call the function by mistake with parentheses:

// Wrong — calls handleClick immediately on every render
<button onClick={handleClick()}>Wrong</button>

// Correct — passes the function reference
<button onClick={handleClick}>Right</button>

Common Event Attributes

Beyond onClick, React supports a wide range of event attributes matching the browser’s event model, all in camelCase.

onChange — Input Changes

function SearchInput() {
  const [query, setQuery] = React.useState('');

  function handleChange(e) {
    setQuery(e.target.value);
  }

  return (
    <div>
      <input
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      <p>You typed: {query}</p>
    </div>
  );
}

onSubmit — Form Submission

function SimpleForm() {
  function handleSubmit(e) {
    e.preventDefault();
    console.log('Form submitted!');
    // output: Form submitted!
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Your name" />
      <button type="submit">Send</button>
    </form>
  );
}

onFocus and onBlur — Focus Management

function FocusDemo() {
  const [focused, setFocused] = React.useState(false);

  return (
    <input
      onFocus={() => setFocused(true)}
      onBlur={() => setFocused(false)}
      placeholder={focused ? 'Focused!' : 'Click here'}
    />
  );
}

onKeyDown — Keyboard Events

function KeyLogger() {
  const [lastKey, setLastKey] = React.useState('');

  function handleKeyDown(e) {
    setLastKey(e.key);
    console.log('Key:', e.key, 'Code:', e.code);
  }

  return (
    <input
      onKeyDown={handleKeyDown}
      placeholder="Press any key"
    />
  );
}

Passing Arguments to Event Handlers

Sometimes you need to pass extra data to your handler. There are two common ways.

Arrow Functions

Wrap the handler in an arrow function:

function ItemList() {
  function handleClick(id) {
    console.log('Clicked item:', id);
    // output: Clicked item: 42
  }

  const items = [
    { id: 1, name: 'Apple' },
    { id: 42, name: 'Banana' },
  ];

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <button onClick={() => handleClick(item.id)}>
            {item.name}
          </button>
        </li>
      ))}
    </ul>
  );
}

The bind Method

bind() pre-fills arguments. The event object always passes as the last argument automatically:

function HandleClick(id) {
  function handleClick(id, event) {
    console.log('ID:', id, 'Target:', event.target.tagName);
    // output: ID: 7 Target: BUTTON
  }

  return <button onClick={handleClick.bind(null, 7)}>Click 7</button>;
}

Passing Both the Event and Extra Data

function handleClick(event, extraData) {
  console.log('Event type:', event.type);  // 'click'
  console.log('Extra:', extraData);         // { foo: 'bar' }
}

<button onClick={(e) => handleClick(e, { foo: 'bar' })}>
  Click
</button>

The Event Object

React passes a synthetic event object as the first argument to every handler. It has all the properties you’d expect from a native event, normalized for cross-browser consistency.

function EventDemo() {
  function handleClick(e) {
    console.log('target:', e.target.tagName);         // BUTTON
    console.log('currentTarget:', e.currentTarget.tagName); // BUTTON
    console.log('type:', e.type);                     // click
    console.log('defaultPrevented:', e.defaultPrevented); // false
  }

  return <button onClick={handleClick}>Click me</button>;
}

preventDefault

Stop the browser’s default behavior:

function StopDefault() {
  function handleClick(e) {
    e.preventDefault();
    console.log('Default prevented, no navigation');
  }

  return (
    <a href="https://example.com" onClick={handleClick}>
      Go to Example
    </a>
  );
}

stopPropagation

Stop the event from bubbling up to parent elements:

function StopProp() {
  function handleDivClick() {
    console.log('Div clicked');
  }

  function handleButtonClick(e) {
    e.stopPropagation();
    console.log('Button clicked only');
  }

  return (
    <div onClick={handleDivClick}>
      <button onClick={handleButtonClick}>Click me</button>
    </div>
  );
}

Without stopPropagation, clicking the button would log both “Button clicked only” and “Div clicked”. With it, only the button’s handler fires.

Working with Forms: Controlled Components

In React, a controlled component is an input whose value lives entirely in state. Every change updates state, and state updates re-render the input with the new value.

Text Input

function TextForm() {
  const [value, setValue] = React.useState('');

  function handleChange(e) {
    setValue(e.target.value);
  }

  function handleSubmit(e) {
    e.preventDefault();
    if (value.trim() === '') {
      console.log('Please enter a value');
      return;
    }
    console.log('Submitted:', value);
    setValue('');
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={value}
        onChange={handleChange}
        placeholder="Your name"
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Select Dropdown

function FruitSelect() {
  const [fruit, setFruit] = React.useState('apple');

  return (
    <select value={fruit} onChange={(e) => setFruit(e.target.value)}>
      <option value="apple">Apple</option>
      <option value="banana">Banana</option>
      <option value="cherry">Cherry</option>
    </select>
  );
}

Checkbox

function ToggleCheckbox() {
  const [checked, setChecked] = React.useState(false);

  return (
    <label>
      <input
        type="checkbox"
        checked={checked}
        onChange={(e) => setChecked(e.target.checked)}
      />
      {checked ? 'Enabled' : 'Disabled'}
    </label>
  );
}

stopPropagation with Nested Elements

Event bubbling is one of the most common sources of confusion in React. When an event fires on a child, it bubbles up to all parent elements that have listeners for that event type.

function NestedDemo() {
  function handleCardClick() {
    console.log('Card clicked');
  }

  function handleButtonClick(e) {
    e.stopPropagation();
    console.log('Button clicked — card handler blocked');
  }

  return (
    <div onClick={handleCardClick} style={{ padding: 20, background: '#eee' }}>
      <p>Click the button, not the card</p>
      <button onClick={handleButtonClick}>Do something</button>
    </div>
  );
}

If you forget stopPropagation, clicking the button logs both messages. This matters a lot when you’re building things like modal overlays where clicking the backdrop should close the modal, but clicking inside the modal content should not.

Custom Events in React

Native browser custom events don’t map to JSX props. You can’t write <div onMyEvent={handler}>. Instead, you attach and dispatch them directly on DOM elements using a ref.

function CustomEventDemo() {
  const ref = React.useRef(null);
  const [log, setLog] = React.useState([]);

  React.useEffect(() => {
    function handleGreet(e) {
      setLog(prev => [...prev, e.detail.message]);
    }

    const el = ref.current;
    el.addEventListener('greet', handleGreet);
    return () => el.removeEventListener('greet', handleGreet);
  }, []);

  function triggerGreet() {
    const event = new CustomEvent('greet', {
      detail: { message: 'Hello from custom event!' },
      bubbles: true,
    });
    ref.current.dispatchEvent(event);
  }

  return (
    <div ref={ref}>
      <button onClick={triggerGreet}>Say Hello</button>
      {log.map((msg, i) => <p key={i}>{msg}</p>)}
    </div>
  );
}

Custom events are useful for communication between unrelated parts of your component tree, or for integrating third-party libraries that emit events.

Summary

React’s event handling system wraps browser events in a Synthetic Event layer that normalizes behavior across browsers and keeps your JSX clean.

Key things to remember:

  • Naming is camelCaseonClick, onChange, onSubmit, not lowercase
  • Pass function referencesonClick={handleClick}, not onClick={handleClick()}
  • Event object is the first argument — access it as e in your handler
  • Call e.preventDefault() to stop browser defaults like form submission or link navigation
  • Call e.stopPropagation() to prevent events from bubbling to parent elements
  • Controlled components drive form inputs entirely through state with onChange updating state and the value prop setting the display
  • Custom events require a DOM ref and addEventListener — they don’t work as JSX props

See Also