nmk

Лекція №7 (2 години). Обробка подій та робота з формами

План лекції

  1. Архітектура подій у React: Синтетичні події (Synthetic Events) та кросбраузерність.
  2. Делегування подій (Event Delegation) у React 17+: зміна парадигми на рівні кореню (Root Node).
  3. Керовані компоненти (Controlled Components): форми як відображення стану (Single Source of Truth).
  4. Некеровані компоненти (Uncontrolled Components) та випадки їх використання.
  5. Хук useRef: мутабельна пам’ять без рендерингу та прямий доступ до DOM-вузлів.
  6. Оптимізація форм: боротьба з перерендерами у складних формах (React Hook Form, Formik).
  7. Патерни та безпека: Debouncing, Throttling та протидія нестандартній поведінці браузера.

Перелік умовних скорочень

Вступ

Інтерактивність сучасних веб-додатків повністю побудована на реагуванні на дії користувача — події. У світі Vanilla JavaScript ми звикли до addEventListener, проте React абстрагує цей процес, створюючи власну паралельну систему подій для забезпечення кросбраузерності та надзвичайно високої продуктивності. Глибоке розуміння цієї системи відрізняє Senior-розробника від початківця. Крім того, робота з формами є одним із найскладніших аспектів React-розробки через постійні компроміси між зручністю розробки (DX) та продуктивністю клієнта.


1. Архітектура подій у React: Синтетичні події (Synthetic Events)

Якщо у React-компоненті ви пишете <button onClick={handleClick}>, ви не вішаєте нативний обробник подій браузера безпосередньо на DOM-вузол кнопки.

Що таке SyntheticEvent? React перехоплює нативну подію браузера (наприклад, MouseEvent) і обгортає її у власний клас — SyntheticEvent.

В 16-й версії React існував механізм Event Pooling (перевикористання об’єктів подій для економії пам’яті), який змушував використовувати e.persist(), щоб мати доступ до події всередині setTimeout. У React 17 від цього пулінгу відмовилися заради спрощення асинхронного коду, тому сучасні синтетичні події поводяться так само, як і нативні.


2. Делегування подій (Event Delegation) у React 17+

Уявіть таблицю на 10 000 користувачів, де кожен рядок має кнопку “Видалити”. Якщо повісити нативний addEventListener на кожну кнопку, браузер використає величезну кількість пам’яті (Memory Leak potential), і сторінка почне “гальмувати”.

Як працює React: React використовує Event Delegation. Замість того, щоб вішати 10 000 обробників, React вішає рівно ОДИН обробник (на подію click) на найвищому рівні. Коли користувач клікає, подія спливає (Bubbling) до цього єдиного обробника, і React самостійно вирішує, який саме компонент має на неї зреагувати на основі власного віртуального дерева (Fiber Tree).

Зміна в React 17: До React 17 цей єдиний глобальний обробник вішався на document. Це викликало конфлікти, якщо на одній сторінці було кілька додатків React або мікс із jQuery. Від React 17 події делегуються на корінь (Root Node) — тобто на той div (зазвичай <div id="root">), у який рендериться ваш конкретний додаток-мікрофронтенд.


3. Керовані компоненти (Controlled Components)

Робота з формами в React ділиться на два кардинально різних підходи. Перший і рекомендований — Керовані компоненти.

Суть: У класичному HTML елементи <input>, <textarea>, <select> самі керують своїм локальним станом на рівні браузера (натиснули клавішу -> браузер зберіг літеру і показав в інпуті). У React ми забираємо цю владу в браузера і передаємо в useState. State стає Єдиним Джерелом Істини (Single Source of Truth).

const ControlledForm = () => {
  const [email, setEmail] = useState("");

  // Обробник стає "шлюзом" - ми можемо валідувати, форматувати (напр. toLowerCase) на льоту
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value.toLowerCase());
  };

  return (
    <input
      type="email"
      value={email} // Інпут показує ТІЛЬКИ те, що дозволяє React
      onChange={handleChange}
    />
  );
};

Переваги: Миттєва валідація, умовне блокування кнопки сабміту (Submit), маскування введення (Input Masking - номери телефонів, кредитні картки).


4. Некеровані компоненти (Uncontrolled Components) та useRef

Іноді повний контроль через useState шкодить. Кожне натискання клавіші в Controlled Component викликає перерендер усієї форми. Якщо форма складається з 50 важких полів, введення тексту почне гальмувати.

Для таких сценаріїв використовують Некеровані компоненти. Їх суть: ми дозволяємо браузеру самостійно керувати станом інпутів, а дані забираємо “на вимогу” (лише в момент натискання кнопки Submit) за допомогою посилань (Refs).

const UncontrolledForm = () => {
  // Створюємо посилання, прив'язуємо до DOM, але це НЕ ВИКЛИКАЄ рендерів
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Отримуємо дані "на вимогу", безпосередньо з DOM
    console.log(fileInputRef.current?.files?.[0]?.name);
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Файл-інпути завжди некеровані в React (через питання безпеки) */}
      <input type="file" ref={fileInputRef} />
      <button type="submit">Upload</button>
    </form>
  );
};

Чому файлові інпути в React завжди некеровані? В React керований компонент означає, що стан (state) є “єдиним джерелом істини” для значення інпуту. Однак для це неможливо через безпеку браузера.

  1. Захист від крадіжки даних Браузери забороняють JavaScript програмно встановлювати значення (value) файлового інпуту.

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

<input type="file" value="C:/Users/Admin/secrets/passwords.txt" />

Після чого скрипт автоматично відправив би форму (form.submit()), викравши ваш файл без вашого відома.

  1. Принцип “Тільки читання” Браузер: Дозволяє лише користувачеві вибрати файл через діалогове вікно.

JavaScript: Може лише прочитати вибраний файл, але не може змінити шлях до нього або вибрати файл за користувача.


5. Хук useRef: мутабельна пам’ять без рендерингу

Хук useRef є архітектурним антиподом (протилежністю) хуку useState. Вимоги до useState: незмінність (Immutability), зміна setState викликає новий рендер. Вимоги до useRef: мутабельність, зміна ref.current НЕ викликає рендер.

У useRef є два абсолютно різних сценарії використання (Use Cases):

1. Доступ до DOM-елементів (Imperative Handle): Для фокусування інпутів (inputRef.current.focus()), інтеграції зі сторонніми бібліотеками, які потребують нативного DOM-вузла (наприклад D3.js для графіків, або Google Maps API).

2. Мутабельний “Сейф” пам’яті (Instance Variables): Як зберігати дані (наприклад ID таймера або попереднє значення пропсів), щоб вони пережили рендер, але зміна цих даних не “смикала” інтерфейс?

const TimerComponent = () => {
  const [ticks, setTicks] = useState(0);
  const timerId = useRef<number | null>(null); // Не спричиняє рендеру

  const start = () => {
    // Ми МУТУЄМО об'єкт напрямую
    timerId.current = window.setInterval(() => setTicks((t) => t + 1), 1000);
  };

  const stop = () => {
    if (timerId.current) clearInterval(timerId.current);
  };

  // ...
};

Правило безпеки: Ніколи не читайте і не записуйте в ref.current безпосередньо в тілі функції-рендера. Це робиться ТІЛЬКИ в обробниках подій (onClick) або всередині useEffect. Інакше ви порушите чистоту компонента.


6. Оптимізація складних форм (React Hook Form / Formik)

Побудова великих корпоративних форм (з валідацією, помилками, брудним станом isDirty, isTouched) на чистих useState перетворюється на непідтримуваний спагеті-код (Spaghetti Code).

Для вирішення цієї інженерної проблеми індустрія використовує бібліотеки управління формами. Світовий стандарт сьогодні — React Hook Form.

Чому не useState? React Hook Form працює на базі некерованих компонентів (useRef), але пропонує синтаксис, схожий на керовані. Перевага феноменальна: при введенні тексту оновлюється лише внутрішній стан бібліотеки (без виклику React Re-render). Рендер екрану відбувається лише тоді, коли з’являється помилка валідації. Це зменшує кількість непотрібних обчислень на порядки.

import { useForm } from "react-hook-form";

const ComplexForm = () => {
  // register "прив'язує" інпут до внутрішнього useRef-накопичувача
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = (data) => console.log(data); // Обробка "на вимогу"

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* Валідація не викликає ререндер на КОЖЕН символ */}
      <input
        {...register("email", { required: true, pattern: /^\S+@\S+$/i })}
      />
      {errors.email && <span>Помилка валідації!</span>}
      <button type="submit">Send</button>
    </form>
  );
};

7. Патерни взаємодії: Debouncing та e.preventDefault()

Дуже часто події трапляються надто часто (наприклад, onChange під час швидкого введення пошукового запиту у поле з автокомплітом, який посилає HTTP-запити на сервер).


Висновки

  1. React “ховає” нативні події браузера за обгорткою SyntheticEvent, що дарує зручність (кросбраузерність) і швидкість (через глобальний механізм Event Delegation на рівні Root-контейнера).
  2. За замовчуванням використовується патерн Керованих компонентів (через useState), оскільки він дає максимальний контроль для миттєвої валідації та залежних полів.
  3. Некеровані компоненти обходять життєвий цикл React-рендеру і є незамінними для оптимізації важких форм (на що спираються інструменти класу React Hook Form) і роботи з input type="file".
  4. Хук useRef є своєрідним “чорним ходом” (Escape Hatch) у декларативній системі React. Він дозволяє будувати мутабельні референси, робота з якими “невидима” для процесів Reconciliation та Rendering. В ідеальному декларативному React-додатку пряме (імперативне) використання useRef для маніпуляцій DOM повинне бути зведено до мінімуму.

Джерела

  1. Офіційна документація React. “Responding to Events”. URL: https://react.dev/learn/responding-to-events
  2. React Docs. “Referencing Values with Refs”. URL: https://react.dev/learn/referencing-values-with-refs
  3. “React 17 Changes to Event Delegation”. React Blog. URL: https://legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html#changes-to-event-delegation
  4. React Hook Form. “Motivation / Performance”. URL: https://react-hook-form.com/
  5. Gosha Arinich. “Controlled and uncontrolled form inputs in React don’t have to be complicated”. URL: https://goshakkk.name/controlled-uncontrolled-inputs-react/
  6. “Debouncing and Throttling in React”. Medium / LogRocket Blog.
  7. Flavio Copes. “The differences between state and refs in React”.
  8. OWASP: “Cross Site Scripting (XSS) Prevention Cheat Sheet”. (В контексті value).

Запитання для самоперевірки

  1. Чому розробники React вирішили відмовитись впроваджувати нативні підписники подій (addEventListener) безпосередньо на кожен вкладений DOM-елемент, і що таке Event Delegation?
  2. Яким чином зміна кріплення системи делегування подій (з document на root div) у React 17 вплинула на архітектуру мікрофронтендів (коли кілька додатків працюють на одній сторінці)?
  3. Опишіть переваги та архітектурні недоліки використання жорстко Керованих компонентів (Controlled) для великих форм (наприклад, 40 полів вводу).
  4. Чому хук useRef називають “сейфом (Escape Hatch) поза системою рендерингу” в парадигмі React? У чому функціональна різниця між ініціалізацією таймера через useRef та через useState?
  5. Надайте обґрунтування: чому суворе правило вимагає “Ніколи не мутувати та не читати ref.current у тілі обчислення функції-компонента”, і дозволяє це робити лише у обробниках подій (onClick) та хуку життєвого циклу (useEffect)?