Интерактивность веб-приложений строится на обработке действий пользователя (клики, ввод текста, отправка форм, прокрутка). В React работа с событиями организована через кроссбраузерную прослойку SyntheticEvent (Синтетические события).
Вместо навешивания отдельных слушателей событий (addEventListener) на каждую отдельную DOM-ноду, React использует архитектурный паттерн делегирования событий (Event Delegation): единый корневой слушатель монтируется на корень контейнера приложения (#root), перехватывает всплывающие нативные события браузера и диспетчеризирует их по виртуальному дереву Fiber.
При отображении динамических коллекций данных (списков) возникает критическая задача согласования элементов при вставке, удалении или сортировке. Для решения этой задачи React требует обязательного задания уникального пропа key. Ключ — это не просто подсказка для линтера, а физический идентификатор узла в алгоритме диффинга (Reconciliation Diffing Algorithm).
Взглянем на то, как алгоритм диффинга видит список с ключами и без ключей:
+-------------------------------------------------------------------------+
| ФИЗИЧЕСКИЙ СМЫСЛ КЛЮЧА key ПРИ ДИФФИНГЕ |
+-------------------------------------------------------------------------+
| СЦЕНАРИЙ: Добавление нового элемента в начало списка |
| Исходный список: [Item B, Item C] |
| Новый список: [Item A, Item B, Item C] |
+-------------------------------------------------------------------------+
| ВАРИАНТ 1: Без ключей (или key={index}): |
| Позиция 0: было B, стало A ---> Мутируем DOM-ноду #0! |
| Позиция 1: было C, стало B ---> Мутируем DOM-ноду #1! |
| Позиция 2: не было, стало C ---> Создаем НОВУЮ DOM-ноду! |
| (ИТОГ: 3 тяжелые мутации DOM, потеря локального стейта инпутов!) |
+-------------------------------------------------------------------------+
| ВАРИАНТ 2: Со стабильными уникальными ключами (key={item.id}): |
| key="a": новый узел ---> Создаем 1 новую DOM-ноду и вставляем|
| key="b": узел существовал ---> Перемещаем существующую DOM-ноду |
| key="c": узел существовал ---> Сохраняем без мутаций! |
| (ИТОГ: 1 быстрая точечная операция, стейт инпутов полностью сохранен!)|
+-------------------------------------------------------------------------+Пользователь кликает по кнопке. Браузер запускает нативный цикл фазы захвата и всплытия (Capture & Bubble).
Единый обработчик React на корне приложения перехватывает событие и упаковывает его в SyntheticEvent.
Диспетчер React находит соответствующий Fiber-узел кнопки и извлекает функцию onClick из memoizedProps.
Выполняется пользовательский код обработчика. Все вызванные setState объединяются в один цикл рендера.
Почему использование индекса массива `key={index}` в динамических списках с сортировкой или удалением считается опасным антипаттерном?
e.preventDefault())..map() (users.map(user => <UserCard key={user.id} user={user} />)).key для очистки формы при смене активного профиля (<EditUserForm key={selectedUserId} />).В React имена событий пишутся в camelCase (onClick, onChange, onSubmit), а в качестве обработчика передается сама ссылка на функцию:
import React, { useState } from 'react';
export function SearchFilter() {
const [query, setQuery] = useState('');
// 1. Типизация события изменения инпута
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
};
// 2. Обработка формы с предотвращением дефолтного сабмита
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Предотвращает перезагрузку браузера!
console.log(`Поиск по запросу: ${query}`);
};
return (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={query}
onChange={handleInputChange}
placeholder="Поиск по каталогу..."
className="px-3 py-2 rounded-lg border border-border bg-bg-surface text-text-primary"
/>
<button type="submit" className="btn-primary">
Найти
</button>
</form>
);
}Каждый элемент коллекции, генерируемый в .map(), обязан иметь стабильный строковый или числовой key:
interface LessonItem {
id: string; // Идеальный кандидат на роль key!
title: string;
durationMinutes: number;
}
interface LessonListProps {
lessons: LessonItem[];
onSelectLesson: (id: string) => void;
}
export function LessonList({ lessons, onSelectLesson }: LessonListProps) {
if (lessons.length === 0) {
return <p className="text-text-muted text-sm">Список уроков пуст</p>;
}
return (
<ul className="divide-y divide-border rounded-xl border border-border bg-bg-surface">
{lessons.map(lesson => (
// key помещается на САМЫЙ ВНЕШНИЙ элемент внутри map!
<li
key={lesson.id}
className="p-4 flex items-center justify-between hover:bg-bg-surface-hover transition-colors"
>
<div>
<h4 className="font-medium text-text-primary">{lesson.title}</h4>
<span className="text-xs text-text-muted">{lesson.durationMinutes} мин</span>
</div>
<button
onClick={() => onSelectLesson(lesson.id)}
className="text-xs text-accent-cyan hover:underline"
>
Изучить →
</button>
</li>
))}
</ul>
);
}key#Ключ key всегда должен указываться непосредственно на корневом элементе, возвращаемом внутри функции обратного вызова .map():
// ПРАВИЛЬНО: ключ на компоненте списка
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
// ПРАВИЛЬНО при использовании фрагмента:
{users.map(user => (
<React.Fragment key={user.id}>
<dt>{user.name}</dt>
<dd>{user.email}</dd>
</React.Fragment>
))}
// ОШИБКА: ключ внутри UserCard НЕ спасет алгоритм сверки внешнего списка!Что произойдет с локальным состоянием формы, если родительский компонент изменит ее проп key с `key='user-1'` на `key='user-2'`?
Генерация ключа во время рендера — катастрофическая ошибка:
// КАТАСТРОФА: при КАЖДОМ ререндере генерируется новый key!
{items.map(item => (
<ListItem key={crypto.randomUUID()} item={item} />
))}React будет уничтожать и пересоздавать DOM-ноды на каждый чих, вызывая мерцание экрана, потерю фокуса и падение производительности. Ключи обязаны быть стабильными между рендерами!
Передача результата вызова функции вместо самой функции:
// ОШИБКА: handleDelete() вызовется СРАЗУ во время рендера, а не при клике!
<button onClick={handleDelete(item.id)}>Удалить</button>
// ПРАВИЛЬНО: передавайте стрелочную функцию-обертку
<button onClick={() => handleDelete(item.id)}>Удалить</button>