React Event Handling

Handle user events in React with camelCase handlers, synthetic events, and controlled inputs.

TL;DR

  1. 01Use camelCase event names like onClick and onChange in JSX.
  2. 02Pass a function reference as the handler, not a function call.
  3. 03Use e.preventDefault() and e.stopPropagation() to control event flow.

Tips

  1. 01Always use controlled components for forms — it gives you better control over input values and validation.

Warnings

  1. 01Remember to call preventDefault() on forms to prevent page reload on submission.

Basic Event Handlers

  • Attach events with camelCase attribute names like onClick, not lowercase onclick.
    function Button() {
      function handleClick() {
        console.log("Button clicked");
      }
    
      return <button onClick={handleClick}>Click me</button>;
    }
    
  • Pass a function reference, not a function call, or it runs on every render.
    // Good: pass the function
    <button onClick={handleClick}>Click</button>
    
    // Bad: calls function immediately on render
    <button onClick={handleClick()}>Click</button>
    
  • Write inline handlers as arrow functions for short, one-off logic.
    <button onClick={() => console.log("clicked")}>Click</button>
    
  • Pass extra arguments to a handler by wrapping the call in an arrow function.
    function List({ items }) {
      return (
        <ul>
          {items.map(item => (
            <li key={item.id}>
              <button onClick={() => handleDelete(item.id)}>
                Delete {item.name}
              </button>
            </li>
          ))}
        </ul>
      );
    }
    
  • Name handler functions with a handle prefix so their purpose is obvious at a glance.
    function handleDelete(id) { /* ... */ }
    function handleToggle() { /* ... */ }
    

Common Event Handlers

  • Handle form submission with onSubmit on the <form> element, not onClick on the button.
    function LoginForm() {
      const handleSubmit = (e) => {
        e.preventDefault();
        console.log("Form submitted");
      };
    
      return <form onSubmit={handleSubmit}></form>;
    }
    
  • Handle input changes with onChange, reading the new value from e.target.value.
    function NameInput() {
      const [name, setName] = useState("");
    
      return <input value={name} onChange={(e) => setName(e.target.value)} />;
    }
    
  • Handle mouse events like onMouseEnter, onMouseLeave, and onMouseMove for hover effects.
    <div onMouseEnter={() => setHovered(true)}>
      Hover me
    </div>
    
  • Handle keyboard shortcuts with onKeyDown, checking e.key for the pressed key.
    function SearchBox() {
      const handleKeyDown = (e) => {
        if (e.key === "Enter") submitSearch();
        if (e.key === "Escape") clearSearch();
      };
    
      return <input onKeyDown={handleKeyDown} placeholder="Search..." />;
    }
    
  • Handle focus and blur events with onFocus and onBlur for accessibility cues.
    function Input() {
      const [focused, setFocused] = useState(false);
    
      return (
        <input
          className={focused ? "focused" : ""}
          onFocus={() => setFocused(true)}
          onBlur={() => setFocused(false)}
        />
      );
    }
    

Event Object

  • Access event properties like target and preventDefault.
    function handleClick(event) {
      console.log(event.target);  // the element clicked
      console.log(event.type);    // "click"
    }
    
    function handleSubmit(event) {
      event.preventDefault();     // stop form submission
      event.stopPropagation();    // stop event bubbling
    }
  • React's SyntheticEvent wraps the browser's native event for cross-browser consistency; access the underlying event with e.nativeEvent.
    function handleClick(e) {
      console.log(e.nativeEvent); // the underlying browser Event
    }
  • As of React 17, synthetic events are no longer pooled and reused: each event object is freshly created and stays valid for as long as you hold a reference to it, even after the handler returns. Calling e.persist() is now a no-op kept only for backward compatibility — you never need to call it.
    // React 16 and earlier nulled out event fields after the handler ran,
    // so reading e.target.value async required e.persist() first.
    // React 17+: the event object is never pooled, so this just works.
    function handleChange(e) {
      setTimeout(() => console.log(e.target.value), 1000);
    }
  • Get input values with e.target.value.
    const handleChange = (e) => {
      setName(e.target.value);
    };
  • Read checkbox state with e.target.checked.
    function Checkbox() {
      const [checked, setChecked] = useState(false);
      
      return (
        <input
          type="checkbox"
          checked={checked}
          onChange={(e) => setChecked(e.target.checked)}
        />
      );
    }
  • Get the key pressed from keyboard events with e.key.
    function handleKeyDown(e) {
      console.log(e.key);    // "Enter", "Escape", "ArrowUp"
      console.log(e.code);   // "Enter", "Escape", "ArrowUp"
      console.log(e.ctrlKey); // true if Ctrl held
    }
  • Access the current target when events bubble up.
    function handleClick(e) {
      console.log(e.target);        // element that was clicked
      console.log(e.currentTarget); // element with the handler
    }

Controlled vs Uncontrolled Components

  • Controlled components: React manages the input value.
    function TextInput() {
      const [value, setValue] = useState("");
      
      return (
        <input 
          value={value}
          onChange={(e) => setValue(e.target.value)}
        />
      );
    }
  • Uncontrolled components: DOM manages the input.
    function TextInput() {
      const inputRef = useRef();
      
      function handleSubmit() {
        console.log(inputRef.current.value);
      }
      
      return (
        <>
          <input ref={inputRef} />
          <button onClick={handleSubmit}>Submit</button>
        </>
      );
    }
  • Use defaultValue for uncontrolled inputs with an initial value.
    // defaultValue sets the initial value but doesn't control updates
    <input defaultValue="Alice" ref={inputRef} />
  • Controlled inputs enable real-time validation and formatting.
    function PhoneInput() {
      const [phone, setPhone] = useState("");
      
      const handleChange = (e) => {
        // Format as user types: 555-123-4567
        const digits = e.target.value.replace(/\D/g, "");
        setPhone(digits.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3"));
      };
      
      return <input value={phone} onChange={handleChange} />;
    }
  • Prefer controlled components for forms that need validation.
    // Controlled: easy to read and validate before submit
    const isValid = email.includes("@") && password.length >= 8;
    <button disabled={!isValid}>Submit</button>

Event Delegation

  • Use event delegation for lists of items.
    function List({ items }) {
      const handleItemClick = (e) => {
        if (e.target.dataset.id) {
          console.log("Item clicked:", e.target.dataset.id);
        }
      };
      
      return (
        <ul onClick={handleItemClick}>
          {items.map(item => (
            <li key={item.id} data-id={item.id}>
              {item.name}
            </li>
          ))}
        </ul>
      );
    }
  • More efficient than adding listeners to each item.
  • Use data-* attributes to identify clicked elements.
  • Use e.target.closest() to find the nearest matching ancestor.
    function handleClick(e) {
      const row = e.target.closest("[data-row-id]");
      if (row) {
        console.log("Row clicked:", row.dataset.rowId);
      }
    }
  • Stop bubbling with e.stopPropagation() to prevent parent handlers.
    function Card() {
      return (
        <div onClick={handleCardClick}>
          Card content
          <button onClick={(e) => {
            e.stopPropagation(); // don't trigger handleCardClick
            handleDeleteClick();
          }}>
            Delete
          </button>
        </div>
      );
    }

FAQ