Hack Frontend Community

Synthetic Events in React

What are Synthetic Events?

Synthetic Events are React's wrapper around native browser events. React creates a unified event system that works the same across all browsers.

function Button() {
  function handleClick(event) {
    console.log(event); // SyntheticEvent, not native Event
  }

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

Why are they needed?

Cross-browser compatibility

Different browsers have different event implementations. Synthetic Events unify the API:

function Input() {
  function handleChange(event) {
    // event.target.value works the same everywhere
    console.log(event.target.value);
  }

  return <input onChange={handleChange} />;
}

Performance

React uses event delegation: all handlers are attached to the root element, not to each element separately.

// Instead of 1000 handlers on each button
// React creates one handler at the root
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <button onClick={() => console.log(item.id)}>
            {item.name}
          </button>
        </li>
      ))}
    </ul>
  );
}

Synthetic Event API

Synthetic Event has the same interface as native events:

function Form() {
  function handleSubmit(event) {
    event.preventDefault(); // Works like native event
    event.stopPropagation();
    
    console.log(event.type); // 'submit'
    console.log(event.target); // element reference
    console.log(event.currentTarget); // element with handler
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

Event Pooling (before React 17)

In React 16 and earlier, events were reused for performance:

// React 16 and earlier
function handleClick(event) {
  console.log(event.type); // 'click'
  
  setTimeout(() => {
    console.log(event.type); // null, event cleared!
  }, 0);
}

To preserve an event, you used event.persist():

function handleClick(event) {
  event.persist(); // Preserve event
  
  setTimeout(() => {
    console.log(event.type); // 'click', works!
  }, 0);
}

React 17+:

In React 17, event pooling was removed. Events are no longer cleared, event.persist() is not needed.


Accessing native event

You can get the native event through nativeEvent:

function handleClick(event) {
  console.log(event); // SyntheticEvent
  console.log(event.nativeEvent); // Native browser Event
  
  // Useful for browser-specific features
  console.log(event.nativeEvent.path);
}

Differences from native events

Naming

React uses camelCase instead of lowercase:

// HTML
<button onclick="handleClick()">Click</button>

// React
<button onClick={handleClick}>Click</button>

Returning false

In HTML you can return false to prevent default action:

<!-- HTML -->
<a href="#" onclick="console.log('clicked'); return false">
  Link
</a>

In React you must explicitly call preventDefault():

// React
function Link() {
  function handleClick(event) {
    event.preventDefault();
    console.log('clicked');
  }

  return <a href="#" onClick={handleClick}>Link</a>;
}

Supported events

React supports all standard DOM events:

Mouse events

onClick
onContextMenu
onDoubleClick
onDrag
onDragEnd
onDragEnter
onDragExit
onDragLeave
onDragOver
onDragStart
onDrop
onMouseDown
onMouseEnter
onMouseLeave
onMouseMove
onMouseOut
onMouseOver
onMouseUp

Keyboard events

function Input() {
  function handleKeyDown(event) {
    if (event.key === 'Enter') {
      console.log('Enter pressed');
    }
  }

  return <input onKeyDown={handleKeyDown} />;
}

Form events

function Form() {
  return (
    <form
      onSubmit={e => e.preventDefault()}
      onChange={e => console.log('changed')}
      onFocus={e => console.log('focused')}
      onBlur={e => console.log('blurred')}
    >
      <input type="text" />
    </form>
  );
}

Focus events

onFocus
onBlur

Touch events

onTouchStart
onTouchMove
onTouchEnd
onTouchCancel

Event delegation

React 16 and earlier

Events were attached to document:

// React attached handlers to document
document.addEventListener('click', handleAllClicks);

React 17+

Events are attached to the React root node:

// React attaches to app root
const root = document.getElementById('root');
root.addEventListener('click', handleAllClicks);

This is important when using multiple React versions on one page or integrating with other libraries.


Event handling specifics

onChange vs onInput

In React onChange works like native onInput — fires on every change:

function Input() {
  const [value, setValue] = useState('');

  return (
    <input
      value={value}
      // Fires on every keystroke
      onChange={e => setValue(e.target.value)}
    />
  );
}

onScroll

onScroll doesn't bubble in React (as in DOM), but React provides it for convenience:

function ScrollableDiv() {
  function handleScroll(event) {
    console.log(event.target.scrollTop);
  }

  return (
    <div onScroll={handleScroll} style={{ height: 200, overflow: 'auto' }}>
      {/* Lots of content */}
    </div>
  );
}

Direct native handler attachment

Sometimes you need to attach a native handler directly:

function Component() {
  const ref = useRef(null);

  useEffect(() => {
    const element = ref.current;
    
    function handleNativeClick(event) {
      console.log('Native click', event);
    }

    element.addEventListener('click', handleNativeClick);

    return () => {
      element.removeEventListener('click', handleNativeClick);
    };
  }, []);

  return <div ref={ref}>Click me</div>;
}

Warning:

Be careful with direct handlers — don't forget to remove them in cleanup function.


Event capturing

React supports the capture phase:

function Parent() {
  return (
    <div
      onClickCapture={() => console.log('1. Parent capture')}
      onClick={() => console.log('3. Parent bubble')}
    >
      <button
        onClickCapture={() => console.log('2. Button capture')}
        onClick={() => console.log('4. Button bubble')}
      >
        Click
      </button>
    </div>
  );
}

// Clicking button outputs:
// 1. Parent capture
// 2. Button capture
// 4. Button bubble
// 3. Parent bubble

TypeScript typing

import { MouseEvent, ChangeEvent, FormEvent } from 'react';

function Component() {
  function handleClick(event: MouseEvent<HTMLButtonElement>) {
    console.log(event.currentTarget.name);
  }

  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    console.log(event.target.value);
  }

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
  }

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

Common mistakes

Asynchronous event usage (React 16)

// Wrong in React 16
function handleClick(event) {
  setTimeout(() => {
    console.log(event.type); // null!
  }, 0);
}

// Correct
function handleClick(event) {
  const eventType = event.type; // Save value
  setTimeout(() => {
    console.log(eventType); // 'click'
  }, 0);
}

Passing event to setState

// Wrong
function handleChange(event) {
  setState(prevState => ({
    ...prevState,
    value: event.target.value // event might be cleared
  }));
}

// Correct
function handleChange(event) {
  const newValue = event.target.value;
  setState(prevState => ({
    ...prevState,
    value: newValue
  }));
}

Conclusion

Synthetic Events:

  • Wrapper around native browser events
  • Provide cross-browser compatibility
  • Use delegation for performance
  • Have the same API as native events
  • In React 17+ are not cleared automatically
  • Use camelCase for naming
  • Native event accessible through nativeEvent

In interviews:

Important to be able to:

  • Explain what Synthetic Events are and why they're needed
  • Describe event delegation
  • Explain differences from native events
  • Describe changes in React 17 (event pooling)
  • Show how to access native event