Реагування на події

React дозволяє додавати обробники подій до вашого JSX. Обробники подій - це ваші власні функції, які будуть спрацьовувати у відповідь на такі взаємодії, як клік, наведення курсору, фокусування введення на формі тощо.

  • Різні способи написання обробника події
  • Як передати логіку обробки подій з батьківського компонента
  • Як поширюються події та як їх зупинити

Додавання обробників подій

Щоб додати обробник події, вам потрібно спочатку визначити функцію, а потім передати її як проп до відповідного JSX-тегу. Наприклад, ось кнопка, яка поки що нічого не робить:

export default function Button() {
  return (
    <button>
      I don't do anything
    </button>
  );
}

Ви можете зробити так, щоб він показував повідомлення, коли користувач клацає, виконавши такі три кроки:

  1. Оголосіть функцію з іменем handleClick всередині вашого Button компонента.
  2. Реалізуйте логіку всередині цієї функції (використовуйте сповіщення для показу повідомлення).
  3. Додати onClick={handleClick} до <button> JSX.
export default function Button() {
  function handleClick() {
    alert('You clicked me!');
  }

  return (
    <button onClick={handleClick}>
      Click me
    </button>
  );
}
button { margin-right: 10px; }

Ви визначили функцію handleClick, а потім передали її як проп до <button>. handleClick є обробником події . Функції обробника події:

  • Зазвичай визначаються всередині ваших компонентів.
  • Мають назви, які починаються з handle, за якими йде назва події.

Зазвичай прийнято називати обробники подій як handle з наступною назвою події. Ви часто бачите onClick={handleClick}, onMouseEnter={handleMouseEnter} і т.д.

Альтернативно, ви можете визначити обробник події в JSX:

<button onClick={function handleClick() {
  alert('You clicked me!');
}}>

Або, більш стисло, за допомогою функції стрілки:

<button onClick={() => {
  alert('You clicked me!');
}}>

Усі ці стилі є еквівалентними. Вбудовані обробники подій зручні для коротких функцій.

Функції, що передаються до обробників подій, слід передавати, а не викликати. Наприклад:

передача функції (правильно) виклик функції (некоректний)
<button onClick={handleClick}> <button onClick={handleClick()}>

Різниця ледь помітна. У першому прикладі функція handleClick передається як обробник події onClick. Це вказує React запам'ятати її та викликати вашу функцію тільки тоді, коли користувач натисне на кнопку.

У другому прикладі () наприкінці handleClick() запускає функцію негайно під час рендерингу, без будь-яких кліків. Це тому, що JavaScript всередині JSX { та } виконується одразу.

При написанні коду в рядок та сама пастка проявляється по-іншому:

передача функції (правильно) виклик функції (некоректний)
<button onClick={() => alert('...')}> <button onClick={alert('...')}>

Такий вбудований код не спрацьовуватиме при натисканні - він спрацьовуватиме щоразу при рендерингу компонента:

// This alert fires when the component renders, not when clicked!
<button onClick={alert('You clicked me!')}>

Якщо ви хочете визначити обробник події вбудовано, обгорніть його в анонімну функцію, наприклад так:

<button onClick={() => alert('You clicked me!')}>

Замість виконання коду всередині під час кожного рендеру створюється функція, яку буде викликано пізніше.

В обох випадках вам потрібно передати функцію:

  • <button onClick={handleClick}> передає функцію handleClick.
  • <button onClick={() => alert('...')}> передає () => alert('...') функцію.

Дізнайтеся більше про функції стрілок.

Читання пропсів в обробниках подій

Оскільки обробники подій оголошуються всередині компонента, вони мають доступ до його пропсів. Ось кнопка, яка при натисканні показує попередження з повідомленням пропу message:

function AlertButton({ message, children }) {
  return (
    <button onClick={() => alert(message)}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <AlertButton message="Playing!">
        Play Movie
      </AlertButton>
      <AlertButton message="Uploading!">
        Upload Image
      </AlertButton>
    </div>
  );
}
button { margin-right: 10px; }

Через це ці дві кнопки показують різні повідомлення. Спробуйте змінити повідомлення, які їм передаються.

Передача обробників подій як пропсів

Часто вам потрібно, щоб батьківський компонент визначав обробник подій дочірнього. Розглянемо кнопки: залежно від того, де ви використовуєте компонент Button, ви можете захотіти виконати іншу функцію - можливо, одна з них відтворює фільм, а інша завантажує зображення.

Для цього передайте проп, який компонент отримує від свого батька, як обробник події, наприклад:

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`Playing ${movieName}!`);
  }

  return (
    <Button onClick={handlePlayClick}>
      Play "{movieName}"
    </Button>
  );
}

function UploadButton() {
  return (
    <Button onClick={() => alert('Uploading!')}>
      Upload Image
    </Button>
  );
}

export default function Toolbar() {
  return (
    <div>
      <PlayButton movieName="Kiki's Delivery Service" />
      <UploadButton />
    </div>
  );
}
button { margin-right: 10px; }

Тут компонент Toolbar рендерить PlayButton та UploadButton:

  • PlayButton передає handlePlayClick як проп onClick до Button всередині.
  • UploadButton передає () => alert('Uploading!') як проп onClick до Button всередині.

Нарешті, ваш компонент Button приймає проп з назвою onClick. Він передає цей проп безпосередньо вбудованому браузеру <button> за допомогою onClick={onClick}. Це вказує React викликати передану функцію при натисканні.

Якщо ви використовуєте систему проектування, то зазвичай компоненти на кшталт кнопок містять стилі, але не визначають поведінку. Натомість, такі компоненти як PlayButton та UploadButton передаватимуть обробники подій вниз.

Називання реквізитів обробника події

Вбудовані компоненти, такі як <button> та <div> підтримують лише назви подій браузера на кшталт onClick. Однак, якщо ви створюєте власні компоненти, ви можете назвати пропси обробників подій як завгодно.

За домовленістю, пропси обробника події мають починатися з on, а потім з великої літери.

Наприклад, проп Button компонента onClick можна було б назвати onSmash:

function Button({ onSmash, children }) {
  return (
    <button onClick={onSmash}>
      {children}
    </button>
  );
}

export default function App() {
  return (
    <div>
      <Button onSmash={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onSmash={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}
button { margin-right: 10px; }

У цьому прикладі <button onClick={onSmash}> показує, що браузеру <button> (у нижньому регістрі) все ще потрібен проп з назвою onClick, але ім'я пропу, яке отримає ваш користувацький Button компонент, залежить від вас!

Якщо ваш компонент підтримує декілька взаємодій, ви можете назвати пропси обробників подій для специфічних для програми концепцій. Наприклад, цей компонент Toolbar отримує обробники подій onPlayMovie та onUploadImage:

export default function App() {
  return (
    <Toolbar
      onPlayMovie={() => alert('Playing!')}
      onUploadImage={() => alert('Uploading!')}
    />
  );
}

function Toolbar({ onPlayMovie, onUploadImage }) {
  return (
    <div>
      <Button onClick={onPlayMovie}>
        Play Movie
      </Button>
      <Button onClick={onUploadImage}>
        Upload Image
      </Button>
    </div>
  );
}

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}
button { margin-right: 10px; }

Зверніть увагу, що компоненту App не потрібно знати , що Toolbar буде робити з onPlayMovie або onUploadImage. Це деталь реалізації Toolbar. Тут Toolbar передає їх як обробники onClick своїм Button, але пізніше він також може викликати їх за допомогою комбінації клавіш. Називання пропсів на честь специфічних для програми взаємодій, як-от onPlayMovie, дає вам змогу гнучко змінювати способи їхнього подальшого використання.

Переконайтеся, що ви використовуєте відповідні теги HTML для обробників подій. Наприклад, для обробки кліків використовуйте <button onClick={handleClick}> замість <div onClick={handleClick}>. Використання справжнього браузера <button> увімкне вбудовану поведінку браузера, наприклад, навігацію за допомогою клавіатури. Якщо вам не подобається стандартний браузерний стиль кнопки і ви хочете, щоб вона виглядала як посилання або інший елемент інтерфейсу, ви можете досягти цього за допомогою CSS. Дізнайтеся більше про написання доступної розмітки.

Поширення подій

Обробники подій також перехоплюватимуть події від будь-яких дочірніх компонентів, які може мати ваш компонент. Ми говоримо, що подія "пузириться" або "поширюється" вгору по дереву: вона починається з місця, де сталася, а потім рухається вгору по дереву.

Цей <div> містить дві кнопки. Обидві кнопки <div> та мають власні обробники onClick. Як ви думаєте, які обробники спрацюють при натисканні на кнопку?

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <button onClick={() => alert('Playing!')}>
        Play Movie
      </button>
      <button onClick={() => alert('Uploading!')}>
        Upload Image
      </button>
    </div>
  );
}
.Toolbar {
  background: #aaa;
  padding: 5px;
}
button { margin: 5px; }

Якщо ви натиснете будь-яку з кнопок, спочатку буде виконано її onClick, а потім onClick батьківського <div>. Таким чином, з'являться два повідомлення. Якщо клацнути на самій панелі інструментів, буде запущено лише батьківський <div> onClick.

Усі події поширюються в React, окрім onScroll, який працює лише на JSX-тегу, до якого ви його додаєте.

Зупинка розмноження

Обробники подій отримують об'єкт події як єдиний аргумент. Зазвичай його називають e, що означає "подія". Ви можете використовувати цей об'єкт для читання інформації про подію.

Цей об'єкт події також дозволяє зупинити поширення. Якщо ви хочете, щоб подія не досягла батьківських компонентів, вам потрібно викликати e.stopPropagation(), як це робить компонент Button:

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

export default function Toolbar() {
  return (
    <div className="Toolbar" onClick={() => {
      alert('You clicked on the toolbar!');
    }}>
      <Button onClick={() => alert('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => alert('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}
.Toolbar {
  background: #aaa;
  padding: 5px;
}
button { margin: 5px; }

При натисканні на кнопку:

  1. React викликає обробник onClick, переданий в <button>.
  2. Цей обробник, визначений у Button, робить наступне:
    • Викликає e.stopPropagation(), запобігаючи подальшому пузирюванню події.
    • Викликає функцію onClick, яка є пропсом, переданим з компонента Toolbar.
  3. Ця функція, визначена у компоненті Toolbar, відображає власне попередження кнопки.
  4. Оскільки розмноження було зупинено, обробник батьківського <div> елемента onClick не запускається.

Внаслідок e.stopPropagation() натискання кнопок тепер показує лише одне сповіщення (з <button>), а не два (з <button> та батьківською панеллю інструментів <div>). Натискання кнопки - це не те саме, що натискання на навколишню панель інструментів, тому зупинка розповсюдження має сенс для цього інтерфейсу.

Захоплення фазових подій

У рідкісних випадках вам може знадобитися перехопити всі події на дочірніх елементах, навіть якщо вони припинили розповсюдження. Наприклад, ви хочете записувати в аналітику кожен клік, незалежно від логіки розповсюдження. Це можна зробити, додавши Capture в кінці назви події:

<div onClickCapture={() => { /* this runs first */ }}>
  <button onClick={e => e.stopPropagation()} />
  <button onClick={e => e.stopPropagation()} />
</div>

Кожна подія поширюється у три фази:

  1. Вона рухається вниз, викликаючи всі onClickCapture обробники.
  2. Він запускає обробник клацнутого елемента onClick.
  3. Він рухається вгору, викликаючи всі onClick обробники.

Події захоплення корисні для коду типу маршрутизаторів або аналітики, але ви, ймовірно, не будете використовувати їх у коді програми.

Передача обробників як альтернатива поширенню

Зверніть увагу, як цей обробник кліку виконує рядок коду , а потім викликає проп onClick, переданий батьком:

function Button({ onClick, children }) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onClick();
    }}>
      {children}
    </button>
  );
}

Ви також можете додати більше коду до цього обробника перед викликом батьківського обробника події onClick. Цей патерн надає альтернативу розмноженню. Він дозволяє дочірньому компоненту обробляти подію, водночас дозволяючи батьківському компоненту вказати деяку додаткову поведінку. На відміну від поширення, це не є автоматичним. Але перевага цього патерну в тому, що ви можете чітко простежити весь ланцюжок коду, який виконується в результаті деякої події.

Якщо ви покладаєтесь на розмноження і вам важко відстежити, які обробники виконуються і чому, спробуйте цей підхід.

Запобігання поведінці за замовчуванням

Деякі події браузера мають поведінку за замовчуванням. Наприклад, подія <form> submit, яка відбувається при натисканні кнопки всередині неї, за замовчуванням перезавантажить всю сторінку:

export default function Signup() {
  return (
    <form onSubmit={() => alert('Submitting!')}>
      <input />
      <button>Send</button>
    </form>
  );
}
button { margin-left: 5px; }

Ви можете викликати e.preventDefault() на об'єкті події, щоб зупинити це:

export default function Signup() {
  return (
    <form onSubmit={e => {
      e.preventDefault();
      alert('Submitting!');
    }}>
      <input />
      <button>Send</button>
    </form>
  );
}
button { margin-left: 5px; }

Не плутайте e.stopPropagation() та e.preventDefault(). Вони обидва корисні, але не пов'язані між собою:

  • e.stopPropagation() зупиняє запуск обробників подій, приєднаних до тегів вище.
  • e.preventDefault() запобігає поведінці браузера за замовчуванням для кількох подій, які її мають.

Чи можуть обробники подій мати побічні ефекти?

Безумовно! Обробники подій - найкраще місце для побічних ефектів.

На відміну від функцій рендерингу, обробники подій не повинні бути чистими, тому це чудове місце для зміни чогось - наприклад, зміни значення входу у відповідь на введення або зміни списку у відповідь на натискання кнопки. Однак, для того, щоб змінити якусь інформацію, вам спочатку потрібно якось її зберегти. У React для цього використовується стан, пам'ять компонента. Ви дізнаєтесь більше про неї на наступній сторінці.

  • Ви можете обробляти події, передавши функцію як проп елементу на кшталт <button>.
  • Обробники подій потрібно передавати, а не викликати! onClick={handleClick}, а не onClick={handleClick()}.
  • Ви можете визначити функцію обробника подій окремо або вбудовано.
  • Обробники подій визначено всередині компонента, тому вони можуть отримати доступ до пропсів.
  • Ви можете оголосити обробник події у батьківському класі і передати його як проп дочірньому.
  • Ви можете визначити власні пропси обробників подій з іменами, специфічними для програми.
  • Події поширюються вгору. Викличте e.stopPropagation() у першому аргументі, щоб запобігти цьому.
  • Події можуть мати небажану поведінку браузера за замовчуванням. Викличте e.preventDefault(), щоб запобігти цьому.
  • Явний виклик пропу обробника події з дочірнього обробника є гарною альтернативою поширенню.

Виправити обробник події

Натискання цієї кнопки має перемикати фон сторінки між білим і чорним. Однак при натисканні нічого не відбувається. Виправте проблему. (Не хвилюйтеся за логіку всередині handleClick - з цією частиною все гаразд.)

export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={handleClick()}>
      Toggle the lights
    </button>
  );
}

Проблема у тому, що <button onClick={handleClick()}> викликає функцію handleClick під час рендерингу замість того, щоб оминати її. Видалення виклику () так, щоб він став <button onClick={handleClick}>, виправляє проблему:

export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={handleClick}>
      Toggle the lights
    </button>
  );
}

Альтернативно, ви можете обгорнути виклик в іншу функцію, наприклад <button onClick={() => handleClick()}>:

export default function LightSwitch() {
  function handleClick() {
    let bodyStyle = document.body.style;
    if (bodyStyle.backgroundColor === 'black') {
      bodyStyle.backgroundColor = 'white';
    } else {
      bodyStyle.backgroundColor = 'black';
    }
  }

  return (
    <button onClick={() => handleClick()}>
      Toggle the lights
    </button>
  );
}

Підключіть події

.

Цей ColorSwitch компонент рендерить кнопку. Вона має змінювати колір сторінки. Підключіть його до пропусків обробника подій onChangeColor, який він отримує від батька, щоб натискання кнопки змінювало колір.

Після того, як ви це зробите, помітьте, що натискання кнопки також збільшує лічильник кліків на сторінці. Ваш колега, який написав батьківський компонент, наполягає, що onChangeColor не збільшує жодного лічильника. Що ще може відбуватися? Виправте так, щоб натискання кнопки лише змінювало колір, а не не збільшувало лічильник.

export default function ColorSwitch({
  onChangeColor
}) {
  return (
    <button>
      Change color
    </button>
  );
}
import { useState } from 'react';
import ColorSwitch from './ColorSwitch.js';

export default function App() {
  const [clicks, setClicks] = useState(0);

  function handleClickOutside() {
    setClicks(c => c + 1);
  }

  function getRandomLightColor() {
    let r = 150 + Math.round(100 * Math.random());
    let g = 150 + Math.round(100 * Math.random());
    let b = 150 + Math.round(100 * Math.random());
    return `rgb(${r}, ${g}, ${b})`;
  }

  function handleChangeColor() {
    let bodyStyle = document.body.style;
    bodyStyle.backgroundColor = getRandomLightColor();
  }

  return (
    <div style={{ width: '100%', height: '100%' }} onClick={handleClickOutside}>
      <ColorSwitch onChangeColor={handleChangeColor} />
      <br />
      <br />
      <h2>Clicks on the page: {clicks}</h2>
    </div>
  );
}

Перш за все, вам потрібно додати обробник події, як <button onClick={onChangeColor}>.

Втім, це призводить до проблеми з лічильником, що збільшується. Якщо onChangeColor не робить цього, як наполягає ваш колега, то проблема в тому, що ця подія поширюється вгору, і це робить якийсь обробник вище. Щоб вирішити цю проблему, потрібно зупинити розповсюдження. Але не забувайте, що все одно слід викликати onChangeColor.

export default function ColorSwitch({
  onChangeColor
}) {
  return (
    <button onClick={e => {
      e.stopPropagation();
      onChangeColor();
    }}>
      Change color
    </button>
  );
}
import { useState } from 'react';
import ColorSwitch from './ColorSwitch.js';

export default function App() {
  const [clicks, setClicks] = useState(0);

  function handleClickOutside() {
    setClicks(c => c + 1);
  }

  function getRandomLightColor() {
    let r = 150 + Math.round(100 * Math.random());
    let g = 150 + Math.round(100 * Math.random());
    let b = 150 + Math.round(100 * Math.random());
    return `rgb(${r}, ${g}, ${b})`;
  }

  function handleChangeColor() {
    let bodyStyle = document.body.style;
    bodyStyle.backgroundColor = getRandomLightColor();
  }

  return (
    <div style={{ width: '100%', height: '100%' }} onClick={handleClickOutside}>
      <ColorSwitch onChangeColor={handleChangeColor} />
      <br />
      <br />
      <h2>Clicks on the page: {clicks}</h2>
    </div>
  );
}