Hack Frontend Community

Synthetic Events в React

Что такое Synthetic Events?

Synthetic Events (Синтетические события) — это обёртка React над нативными событиями браузера. React создаёт единую систему событий, которая работает одинаково во всех браузерах.

function Button() {
  function handleClick(event) {
    console.log(event); // SyntheticEvent, не нативный Event
  }

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

Зачем нужны?

Кроссбраузерность

Разные браузеры имеют разные реализации событий. Synthetic Events унифицируют API:

function Input() {
  function handleChange(event) {
    // event.target.value работает одинаково везде
    console.log(event.target.value);
  }

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

Производительность

React использует делегирование событий: все обработчики прикрепляются к корневому элементу, а не к каждому элементу отдельно.

// Вместо 1000 обработчиков на каждой кнопке
// React создаёт один обработчик на корне
function List({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          <button onClick={() => console.log(item.id)}>
            {item.name}
          </button>
        </li>
      ))}
    </ul>
  );
}

API Synthetic Events

Synthetic Event имеет тот же интерфейс, что и нативное событие:

function Form() {
  function handleSubmit(event) {
    event.preventDefault(); // Работает как в нативном событии
    event.stopPropagation();
    
    console.log(event.type); // 'submit'
    console.log(event.target); // ссылка на элемент
    console.log(event.currentTarget); // ссылка на элемент с обработчиком
  }

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

Event Pooling (до React 17)

В React 16 и ранее события переиспользовались для производительности:

// React 16 и ранее
function handleClick(event) {
  console.log(event.type); // 'click'
  
  setTimeout(() => {
    console.log(event.type); // null, событие очищено!
  }, 0);
}

Если нужно было сохранить событие, использовали event.persist():

function handleClick(event) {
  event.persist(); // Сохраняем событие
  
  setTimeout(() => {
    console.log(event.type); // 'click', работает!
  }, 0);
}

React 17+:

В React 17 event pooling убрали. События больше не очищаются, event.persist() не нужен.


Доступ к нативному событию

Можно получить нативное событие через nativeEvent:

function handleClick(event) {
  console.log(event); // SyntheticEvent
  console.log(event.nativeEvent); // Нативный Event браузера
  
  // Полезно для специфичных возможностей браузера
  console.log(event.nativeEvent.path);
}

Различия с нативными событиями

Именование

React использует camelCase вместо lowercase:

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

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

Возврат false

В HTML можно вернуть false для предотвращения действия по умолчанию:

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

В React нужно явно вызывать preventDefault():

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

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

Поддерживаемые события

React поддерживает все стандартные события DOM:

События мыши

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

События клавиатуры

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

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

События формы

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>
  );
}

События фокуса

onFocus
onBlur

События touch

onTouchStart
onTouchMove
onTouchEnd
onTouchCancel

Делегирование событий

React 16 и ранее

События прикреплялись к document:

// React прикреплял обработчики к document
document.addEventListener('click', handleAllClicks);

React 17+

События прикрепляются к корневому React-узлу:

// React прикрепляет к корню приложения
const root = document.getElementById('root');
root.addEventListener('click', handleAllClicks);

Это важно при использовании нескольких версий React на одной странице или при интеграции с другими библиотеками.


Особенности работы с событиями

onChange vs onInput

В React onChange работает как нативный onInput — срабатывает при каждом изменении:

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

  return (
    <input
      value={value}
      // Срабатывает при каждом нажатии клавиши
      onChange={e => setValue(e.target.value)}
    />
  );
}

onScroll

onScroll не всплывает в React (как и в DOM), но React предоставляет его для удобства:

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

  return (
    <div onScroll={handleScroll} style={{ height: 200, overflow: 'auto' }}>
      {/* Много контента */}
    </div>
  );
}

Прямое добавление нативных обработчиков

Иногда нужно добавить нативный обработчик напрямую:

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>;
}

Внимание:

Будьте осторожны с прямыми обработчиками — не забывайте их удалять в cleanup функции.


Захват событий

React поддерживает фазу захвата (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>
  );
}

// При клике на кнопку выведет:
// 1. Parent capture
// 2. Button capture
// 4. Button bubble
// 3. Parent bubble

Типизация в TypeScript

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>
  );
}

Частые ошибки

Асинхронное использование события (React 16)

// Неправильно в React 16
function handleClick(event) {
  setTimeout(() => {
    console.log(event.type); // null!
  }, 0);
}

// Правильно
function handleClick(event) {
  const eventType = event.type; // Сохраняем значение
  setTimeout(() => {
    console.log(eventType); // 'click'
  }, 0);
}

Передача события в setState

// Неправильно
function handleChange(event) {
  setState(prevState => ({
    ...prevState,
    value: event.target.value // event может быть очищен
  }));
}

// Правильно
function handleChange(event) {
  const newValue = event.target.value;
  setState(prevState => ({
    ...prevState,
    value: newValue
  }));
}

Вывод

Synthetic Events:

  • Обёртка над нативными событиями браузера
  • Обеспечивают кроссбраузерность
  • Используют делегирование для производительности
  • Имеют тот же API, что и нативные события
  • В React 17+ не очищаются автоматически
  • Используют camelCase для именования
  • Можно получить нативное событие через nativeEvent

На собеседовании:

Важно уметь:

  • Объяснить, что такое Synthetic Events и зачем они нужны
  • Рассказать о делегировании событий
  • Объяснить разницу с нативными событиями
  • Рассказать об изменениях в React 17 (event pooling)
  • Показать, как получить нативное событие