← React/События в React#261 из 383← ПредыдущийСледующий →+20 XP
Полезно по теме:Гайд: React или VueПрактика: React setТермин: React HooksТема: React: хуки и экосистема

События в React

Синтетические события (SyntheticEvent)

React не использует нативные DOM-события напрямую. Вместо этого он создаёт SyntheticEvent — кросс-браузерную обёртку над нативным событием. Интерфейс синтетического события идентичен нативному (event.target, event.preventDefault(), event.stopPropagation()), но работает одинаково во всех браузерах.

function Form() {
  function handleSubmit(event) {
    event.preventDefault()   // работает так же как в нативном JS
    console.log(event.target)  // DOM-элемент формы
    console.log(event.nativeEvent)  // настоящий браузерный Event
  }

  return <form onSubmit={handleSubmit}>...</form>
}

Обработчики событий как props

В React события — это props с именами в camelCase. Значение — функция (не строка как в HTML):

// HTML (старый способ — строка!):
<button onclick="handleClick()">Кнопка</button>

// React (функция):
<button onClick={handleClick}>Кнопка</button>
<input onChange={handleChange} />
<form onSubmit={handleSubmit} />
<div onMouseEnter={handleHover} onMouseLeave={handleLeave} />
<input onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} />

Паттерны обработчиков

Именованная функция (рекомендуется для сложной логики):

function Button({ id, onDelete }) {
  function handleClick(event) {
    event.stopPropagation()
    onDelete(id)
  }
  return <button onClick={handleClick}>Удалить</button>
}

Стрелочная функция inline (удобна для коротких выражений):

<button onClick={() => setCount(c => c + 1)}>+</button>
<input onChange={(e) => setName(e.target.value)} />

Передача аргументов — через стрелочную функцию-обёртку:

// Передаём id в обработчик
{items.map(item => (
  <li key={item.id}>
    {item.text}
    <button onClick={() => handleDelete(item.id)}>×</button>
  </li>
))}

Нельзя писать onClick={handleDelete(item.id)} — это вызов функции, а не передача! Нужна обёртка () => handleDelete(item.id).

event.preventDefault() и event.stopPropagation()

// preventDefault — отменяет стандартное поведение браузера
function LoginForm() {
  function handleSubmit(e) {
    e.preventDefault()  // без этого страница перезагрузится!
    // наша логика...
  }
  return <form onSubmit={handleSubmit}>...</form>
}

// stopPropagation — останавливает всплытие события
function Modal({ onClose, children }) {
  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        {/* клик внутри модала не закроет его */}
        {children}
      </div>
    </div>
  )
}

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

Браузерный addEventListener добавляет слушатель на каждый элемент. React использует делегирование событий: один обработчик на корневой элемент (document или #root), который перехватывает все события через всплытие.

// Vanilla JS — обработчик на каждую кнопку:
document.querySelectorAll('button').forEach(btn => {
  btn.addEventListener('click', handler)  // N обработчиков
})

// React — один обработчик на корне:
document.getElementById('root').addEventListener('click', reactEventHandler)
// React сам определяет какой компонент вызвать

Это эффективнее: список из 1000 элементов не создаёт 1000 слушателей.

Часто используемые события

| JSX prop | Нативное событие | Применение |

|---|---|---|

| onClick | click | кнопки, ссылки |

| onChange | change (input, select) | поля ввода |

| onSubmit | submit | формы |

| onKeyDown | keydown | горячие клавиши |

| onKeyUp | keyup | обработка ввода |

| onFocus / onBlur | focus / blur | поля форм |

| onMouseEnter | mouseenter | всплывающие подсказки |

| onScroll | scroll | бесконечная прокрутка |

TypeScript типизация событий

// React.MouseEvent<HTMLButtonElement> — тип события клика на кнопку
function Button({ onClick }: { onClick: React.MouseEvent<HTMLButtonElement> }) { ... }

// Правильный способ типизировать обработчики:
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }

Примеры

Делегирование событий и паттерны обработчиков: от addEventListener к React-стилю

// ============================================================
// Часть 1: Делегирование событий — как React экономит память
// ============================================================

// VANILLA JS: отдельный listener на каждый элемент (дорого!)
function addListenersNaive(items) {
  let listenerCount = 0
  items.forEach(item => {
    const el = document.createElement('button')
    el.textContent = item.text
    el.addEventListener('click', () => {
      console.log(`Нажато: ${item.text}`)
    })
    listenerCount++
  })
  console.log(`Naive: создано ${listenerCount} слушателей для ${items.length} элементов`)
}

// ДЕЛЕГИРОВАНИЕ (как React): один listener на родителе
function addListenersDelegated(container, items) {
  // Один обработчик на весь список
  container.addEventListener('click', (event) => {
    const button = event.target.closest('[data-id]')
    if (!button) return
    const id = button.dataset.id
    const item = items.find(i => String(i.id) === id)
    if (item) console.log(`[Делегирование] Нажато: ${item.text}`)
  })

  items.forEach(item => {
    const el = document.createElement('button')
    el.dataset.id = item.id
    el.textContent = item.text
    container.appendChild(el)
  })
  console.log(`Delegation: создан 1 слушатель для ${items.length} элементов`)
}

// Демонстрация разницы
const testItems = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, text: `Элемент ${i + 1}` }))

console.log('=== Сравнение подходов ===')
addListenersNaive(testItems)            // 10 слушателей

const container = document.createElement('div')
addListenersDelegated(container, testItems)  // 1 слушатель

// ============================================================
// Часть 2: Паттерны передачи аргументов в обработчики
// ============================================================

// Имитируем React-компонент с обработчиком событий
function createTodoItem(todo, onDelete, onToggle) {
  // React JSX эквивалент:
  // return (
  //   <li>
  //     <span onClick={() => onToggle(todo.id)}>{todo.text}</span>
  //     <button onClick={(e) => { e.stopPropagation(); onDelete(todo.id) }}>×</button>
  //   </li>
  // )

  // ВАЖНО: onClick={() => onDelete(todo.id)}
  // НЕ: onClick={onDelete(todo.id)} — это вызов, а не передача!
  return {
    text: todo.text,
    onSpanClick: () => onToggle(todo.id),        // передача аргумента через стрелку
    onButtonClick: (e) => {
      // e.stopPropagation() — предотвращаем всплытие к span
      console.log('[stopPropagation] клик по кнопке не достигнет span')
      onDelete(todo.id)
    }
  }
}

// Имитируем form onSubmit с preventDefault
function createLoginForm(onSubmit) {
  return {
    handleSubmit(event) {
      // В реальном React: event.preventDefault()
      // У нас нет реального form, симулируем:
      if (event && event.preventDefault) event.preventDefault()

      const formData = event?.formData || { email: 'test@test.com', password: '123456' }
      console.log('[Form] Отправка предотвращена, данные:', formData)
      onSubmit(formData)
    }
  }
}

// Демонстрация
const todo = { id: 1, text: 'Изучить обработчики событий React' }
const item = createTodoItem(
  todo,
  (id) => console.log(`Удалён todo #${id}`),
  (id) => console.log(`Переключён todo #${id}`)
)

console.log('\n=== Обработчики событий ===')
item.onSpanClick()     // Переключён todo #1
item.onButtonClick()   // [stopPropagation] + Удалён todo #1

const form = createLoginForm((data) => console.log('Логин:', data.email))
form.handleSubmit({ preventDefault: () => {}, formData: { email: 'user@react.dev' } })

События в React

Синтетические события (SyntheticEvent)

React не использует нативные DOM-события напрямую. Вместо этого он создаёт SyntheticEvent — кросс-браузерную обёртку над нативным событием. Интерфейс синтетического события идентичен нативному (event.target, event.preventDefault(), event.stopPropagation()), но работает одинаково во всех браузерах.

function Form() {
  function handleSubmit(event) {
    event.preventDefault()   // работает так же как в нативном JS
    console.log(event.target)  // DOM-элемент формы
    console.log(event.nativeEvent)  // настоящий браузерный Event
  }

  return <form onSubmit={handleSubmit}>...</form>
}

Обработчики событий как props

В React события — это props с именами в camelCase. Значение — функция (не строка как в HTML):

// HTML (старый способ — строка!):
<button onclick="handleClick()">Кнопка</button>

// React (функция):
<button onClick={handleClick}>Кнопка</button>
<input onChange={handleChange} />
<form onSubmit={handleSubmit} />
<div onMouseEnter={handleHover} onMouseLeave={handleLeave} />
<input onKeyDown={handleKeyDown} onKeyUp={handleKeyUp} />

Паттерны обработчиков

Именованная функция (рекомендуется для сложной логики):

function Button({ id, onDelete }) {
  function handleClick(event) {
    event.stopPropagation()
    onDelete(id)
  }
  return <button onClick={handleClick}>Удалить</button>
}

Стрелочная функция inline (удобна для коротких выражений):

<button onClick={() => setCount(c => c + 1)}>+</button>
<input onChange={(e) => setName(e.target.value)} />

Передача аргументов — через стрелочную функцию-обёртку:

// Передаём id в обработчик
{items.map(item => (
  <li key={item.id}>
    {item.text}
    <button onClick={() => handleDelete(item.id)}>×</button>
  </li>
))}

Нельзя писать onClick={handleDelete(item.id)} — это вызов функции, а не передача! Нужна обёртка () => handleDelete(item.id).

event.preventDefault() и event.stopPropagation()

// preventDefault — отменяет стандартное поведение браузера
function LoginForm() {
  function handleSubmit(e) {
    e.preventDefault()  // без этого страница перезагрузится!
    // наша логика...
  }
  return <form onSubmit={handleSubmit}>...</form>
}

// stopPropagation — останавливает всплытие события
function Modal({ onClose, children }) {
  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        {/* клик внутри модала не закроет его */}
        {children}
      </div>
    </div>
  )
}

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

Браузерный addEventListener добавляет слушатель на каждый элемент. React использует делегирование событий: один обработчик на корневой элемент (document или #root), который перехватывает все события через всплытие.

// Vanilla JS — обработчик на каждую кнопку:
document.querySelectorAll('button').forEach(btn => {
  btn.addEventListener('click', handler)  // N обработчиков
})

// React — один обработчик на корне:
document.getElementById('root').addEventListener('click', reactEventHandler)
// React сам определяет какой компонент вызвать

Это эффективнее: список из 1000 элементов не создаёт 1000 слушателей.

Часто используемые события

| JSX prop | Нативное событие | Применение |

|---|---|---|

| onClick | click | кнопки, ссылки |

| onChange | change (input, select) | поля ввода |

| onSubmit | submit | формы |

| onKeyDown | keydown | горячие клавиши |

| onKeyUp | keyup | обработка ввода |

| onFocus / onBlur | focus / blur | поля форм |

| onMouseEnter | mouseenter | всплывающие подсказки |

| onScroll | scroll | бесконечная прокрутка |

TypeScript типизация событий

// React.MouseEvent<HTMLButtonElement> — тип события клика на кнопку
function Button({ onClick }: { onClick: React.MouseEvent<HTMLButtonElement> }) { ... }

// Правильный способ типизировать обработчики:
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => { ... }
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { ... }

Примеры

Делегирование событий и паттерны обработчиков: от addEventListener к React-стилю

// ============================================================
// Часть 1: Делегирование событий — как React экономит память
// ============================================================

// VANILLA JS: отдельный listener на каждый элемент (дорого!)
function addListenersNaive(items) {
  let listenerCount = 0
  items.forEach(item => {
    const el = document.createElement('button')
    el.textContent = item.text
    el.addEventListener('click', () => {
      console.log(`Нажато: ${item.text}`)
    })
    listenerCount++
  })
  console.log(`Naive: создано ${listenerCount} слушателей для ${items.length} элементов`)
}

// ДЕЛЕГИРОВАНИЕ (как React): один listener на родителе
function addListenersDelegated(container, items) {
  // Один обработчик на весь список
  container.addEventListener('click', (event) => {
    const button = event.target.closest('[data-id]')
    if (!button) return
    const id = button.dataset.id
    const item = items.find(i => String(i.id) === id)
    if (item) console.log(`[Делегирование] Нажато: ${item.text}`)
  })

  items.forEach(item => {
    const el = document.createElement('button')
    el.dataset.id = item.id
    el.textContent = item.text
    container.appendChild(el)
  })
  console.log(`Delegation: создан 1 слушатель для ${items.length} элементов`)
}

// Демонстрация разницы
const testItems = Array.from({ length: 10 }, (_, i) => ({ id: i + 1, text: `Элемент ${i + 1}` }))

console.log('=== Сравнение подходов ===')
addListenersNaive(testItems)            // 10 слушателей

const container = document.createElement('div')
addListenersDelegated(container, testItems)  // 1 слушатель

// ============================================================
// Часть 2: Паттерны передачи аргументов в обработчики
// ============================================================

// Имитируем React-компонент с обработчиком событий
function createTodoItem(todo, onDelete, onToggle) {
  // React JSX эквивалент:
  // return (
  //   <li>
  //     <span onClick={() => onToggle(todo.id)}>{todo.text}</span>
  //     <button onClick={(e) => { e.stopPropagation(); onDelete(todo.id) }}>×</button>
  //   </li>
  // )

  // ВАЖНО: onClick={() => onDelete(todo.id)}
  // НЕ: onClick={onDelete(todo.id)} — это вызов, а не передача!
  return {
    text: todo.text,
    onSpanClick: () => onToggle(todo.id),        // передача аргумента через стрелку
    onButtonClick: (e) => {
      // e.stopPropagation() — предотвращаем всплытие к span
      console.log('[stopPropagation] клик по кнопке не достигнет span')
      onDelete(todo.id)
    }
  }
}

// Имитируем form onSubmit с preventDefault
function createLoginForm(onSubmit) {
  return {
    handleSubmit(event) {
      // В реальном React: event.preventDefault()
      // У нас нет реального form, симулируем:
      if (event && event.preventDefault) event.preventDefault()

      const formData = event?.formData || { email: 'test@test.com', password: '123456' }
      console.log('[Form] Отправка предотвращена, данные:', formData)
      onSubmit(formData)
    }
  }
}

// Демонстрация
const todo = { id: 1, text: 'Изучить обработчики событий React' }
const item = createTodoItem(
  todo,
  (id) => console.log(`Удалён todo #${id}`),
  (id) => console.log(`Переключён todo #${id}`)
)

console.log('\n=== Обработчики событий ===')
item.onSpanClick()     // Переключён todo #1
item.onButtonClick()   // [stopPropagation] + Удалён todo #1

const form = createLoginForm((data) => console.log('Логин:', data.email))
form.handleSubmit({ preventDefault: () => {}, formData: { email: 'user@react.dev' } })

Задание

Создай компонент App с кнопкой и полем ввода. При нажатии кнопки значение из поля добавляется в список сообщений внизу. При нажатии Enter в поле — тоже добавляется. После добавления поле очищается. Используй useState для хранения текста и массива сообщений.

Подсказка

useState([]) для массива. В handleAdd проверяй text.trim(). onChange читает e.target.value. onKeyDown проверяет e.key === "Enter". onClick на кнопку — просто {handleAdd} без скобок. setMessages использует функциональное обновление с prev.

Загружаем среду выполнения...
Загружаем AI-помощника...