React Front-end Инженер

React Front-end Инженер

Роадмап навыков для прокачки

Работа со всплытием в компонентах

ReactСобытияВсплытие событий в React

Основная идея

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

Ключевые аспекты

  • stopPropagation() — остановка всплытия к родительским элементам
  • preventDefault() — отмена действия браузера по умолчанию
  • Делегирование — один обработчик на родителе для всех детей
  • event.target vs event.currentTarget — цель события vs элемент с обработчиком
  • Модальные окна — классический кейс с кликом по overlay

Плюсы правильной работы с всплытием

  • Гибкое управление поведением вложенных компонентов
  • Оптимизация через делегирование событий
  • Предсказуемое поведение интерактивных элементов

Минусы неправильного использования

  • Неожиданные срабатывания обработчиков
  • Сложная отладка вложенных событий
  • Избыточное использование stopPropagation

Частые ошибки на собеседованиях

  • Путают target и currentTarget
  • Злоупотребляют stopPropagation вместо правильной структуры
  • Не понимают, когда использовать делегирование
  • Забывают про preventDefault для ссылок и форм

Введение и проблематика

Понимание теории всплытия — это хорошо, но на практике важно уметь применять эти знания для решения реальных задач: закрытие модальных окон, обработка кликов в списках, предотвращение нежелательных срабатываний.

Эффективная работа со всплытием — это баланс между использованием делегирования и точечным контролем событий.


Основные инструменты

event.target vs event.currentTarget

Code Example 1: Чем отличается e.target от e.currentTarget? Какие значения будут при клике на кнопку?

tsx
function TargetExample() {
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    console.log('target:', e.target);        // Элемент, ГДЕ произошёл клик
    console.log('currentTarget:', e.currentTarget); // Элемент С обработчиком
  };
 
  return (
    <div onClick={handleClick} className="container">
      <button>Кнопка</button>
    </div>
  );
}
 
// При клике на кнопку:
// target: <button>Кнопка</button>
// currentTarget: <div class="container">...</div>
СвойствоОписание
e.targetЭлемент, на котором изначально произошло событие
e.currentTargetЭлемент, на котором висит обработчик

stopPropagation vs stopImmediatePropagation

Code Example 2: Что выведется в консоль при клике на кнопку? Какие обработчики НЕ сработают?

tsx
function PropagationExample() {
  const div1Handler = () => console.log('div handler 1');
  const div2Handler = () => console.log('div handler 2');
 
  const buttonHandler = (e: React.MouseEvent) => {
    e.stopPropagation();
    // Останавливает всплытие к родителям,
    // но другие обработчики на этом же элементе сработают
    console.log('button');
  };
 
  return (
    <div onClick={div1Handler}>
      <div onClick={div2Handler}>
        <button onClick={buttonHandler}>Клик</button>
      </div>
    </div>
  );
}
 
// При клике: "button"
// div handler 1 и div handler 2 НЕ вызовутся

Практические паттерны

Паттерн 1: Модальное окно с overlay

Классическая задача — закрытие модалки при клике на затемнённый фон, но не при клике внутри:

Code Example 3: В чём разница между этими двумя способами? Какой предпочтительнее и почему?

tsx
function Modal({ onClose, children }) {
  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>
  );
}

Второй способ (проверка target) предпочтительнее, так как не блокирует всплытие и позволяет другим обработчикам получить событие.

Паттерн 2: Карточка с кнопкой действия

Вся карточка кликабельна, но внутри есть кнопка с отдельным действием:

Code Example 4: Зачем здесь stopPropagation? Что произойдёт при клике на "В корзину" без него?

tsx
function ProductCard({ product, onSelect, onAddToCart }) {
  const handleCardClick = () => {
    onSelect(product.id);
  };
 
  const handleAddClick = (e: React.MouseEvent) => {
    e.stopPropagation(); // Не открываем карточку при клике на кнопку
    onAddToCart(product.id);
  };
 
  return (
    <div className="card" onClick={handleCardClick}>
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price} ₽</p>
      <button onClick={handleAddClick}>В корзину</button>
    </div>
  );
}

Паттерн 3: Dropdown меню

Закрытие dropdown при клике вне его:

Code Example 5: Как работает закрытие dropdown при клике вне его? Зачем проверять contains()?

tsx
function Dropdown({ trigger, children }) {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
        setIsOpen(false);
      }
    };
 
    if (isOpen) {
      document.addEventListener('click', handleClickOutside);
    }
 
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, [isOpen]);
 
  return (
    <div ref={dropdownRef}>
      <button onClick={() => setIsOpen(!isOpen)}>{trigger}</button>
      {isOpen && <div className="dropdown-menu">{children}</div>}
    </div>
  );
}

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

Эффективная обработка списков

Вместо N обработчиков на N элементах — один обработчик на контейнере:

Code Example 6: Как работает closest() в этом примере? Зачем он нужен вместо проверки tagName?

tsx
interface Item {
  id: number;
  name: string;
}
 
function ItemList({ items }: { items: Item[] }) {
  const handleClick = (e: React.MouseEvent<HTMLUListElement>) => {
    const target = e.target as HTMLElement;
    const li = target.closest('li');
 
    if (!li) return;
 
    const id = li.dataset.id;
    const action = target.dataset.action;
 
    switch (action) {
      case 'edit':
        console.log('Редактировать:', id);
        break;
      case 'delete':
        console.log('Удалить:', id);
        break;
      default:
        console.log('Выбрать:', id);
    }
  };
 
  return (
    <ul onClick={handleClick}>
      {items.map(item => (
        <li key={item.id} data-id={item.id}>
          <span>{item.name}</span>
          <button data-action="edit">✏️</button>
          <button data-action="delete">🗑️</button>
        </li>
      ))}
    </ul>
  );
}
⚠️

Используйте closest() для поиска нужного родительского элемента — это надёжнее, чем проверять tagName напрямую.


Работа с формами

preventDefault для отмены отправки

tsx
function LoginForm() {
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault(); // Отменяем стандартную отправку формы
 
    // Своя логика отправки
    const formData = new FormData(e.currentTarget as HTMLFormElement);
    console.log('Email:', formData.get('email'));
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      <button type="submit">Войти</button>
    </form>
  );
}

Ссылки внутри кликабельных элементов

tsx
function ArticleCard({ article }) {
  const handleCardClick = () => {
    // Переход к статье
  };
 
  return (
    <div className="card" onClick={handleCardClick}>
      <h3>{article.title}</h3>
      <p>{article.excerpt}</p>
      <a
        href={article.authorUrl}
        onClick={(e) => e.stopPropagation()} // Ссылка работает независимо
      >
        {article.author}
      </a>
    </div>
  );
}

Пограничные кейсы

Порталы и всплытие

События из порталов (ReactDOM.createPortal) всплывают по React-дереву, а не по DOM-дереву!

Code Example 7: Почему клик по кнопке в портале вызовет "Parent clicked"? Ведь в DOM кнопка находится в другом месте?

tsx
function Parent() {
  // Этот обработчик поймает событие из портала
  const handleClick = () => console.log('Parent clicked');
 
  return (
    <div onClick={handleClick}>
      <PortalChild />
    </div>
  );
}
 
function PortalChild() {
  return ReactDOM.createPortal(
    <button>Кнопка в портале</button>,
    document.getElementById('portal-root')!
  );
}
 
// Клик по кнопке вызовет "Parent clicked",
// хотя в DOM кнопка находится в #portal-root

Асинхронные операции и SyntheticEvent

tsx
function AsyncExample() {
  const handleClick = async (e: React.MouseEvent) => {
    // ❌ Неправильно — после await event уже очищен
    await someAsyncOperation();
    console.log(e.type); // Может быть null
 
    // ✅ Правильно — сохраняем нужные данные до await
    const eventType = e.type;
    await someAsyncOperation();
    console.log(eventType);
  };
 
  return <button onClick={handleClick}>Клик</button>;
}

Антипаттерны

Избыточный stopPropagation

Code Example 8: Почему Version A считается антипаттерном? Чем лучше Version B?

tsx
// ❌ Плохо — блокирует всплытие везде
function BadComponent() {
  return (
    <div onClick={(e) => { e.stopPropagation(); doSomething(); }}>
      <span onClick={(e) => { e.stopPropagation(); doOther(); }}>
        <button onClick={(e) => { e.stopPropagation(); doAction(); }}>
          Клик
        </button>
      </span>
    </div>
  );
}
 
// ✅ Хорошо — используем stopPropagation только где нужно
function GoodComponent() {
  return (
    <div onClick={handleContainer}>
      <button onClick={(e) => { e.stopPropagation(); handleAction(); }}>
        Действие
      </button>
    </div>
  );
}

Вопросы интервьюера

Q: Как определить, на каком элементе произошёл клик?

Используйте e.target для получения элемента, где событие изначально произошло. e.currentTarget — элемент с обработчиком.

Q: Как закрыть модалку при клике на overlay, но не на содержимое?

Либо stopPropagation на контенте, либо проверка e.target === e.currentTarget на overlay.

Q: Когда использовать делегирование событий?

При большом количестве однотипных элементов (списки, таблицы). Это экономит память и упрощает динамическое добавление элементов.

Q: Почему событие из портала всплывает к React-родителю?

React сохраняет логическую иерархию компонентов независимо от DOM. Это позволяет обрабатывать события единообразно.


Источники

Code Example 1: target vs currentTarget

❓ Чем отличается e.target от e.currentTarget? Какие значения будут при клике на кнопку?

tsx
function TargetExample() {
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    console.log('target:', e.target);
    console.log('currentTarget:', e.currentTarget);
  };
 
  return (
    <div onClick={handleClick} className="container">
      <button>Кнопка</button>
    </div>
  );
}

Code Example 2: stopPropagation example

❓ Что выведется в консоль при клике на кнопку? Какие обработчики НЕ сработают?

tsx
function PropagationExample() {
  const div1Handler = () => console.log('div handler 1');
  const div2Handler = () => console.log('div handler 2');
 
  const buttonHandler = (e: React.MouseEvent) => {
    e.stopPropagation();
    console.log('button');
  };
 
  return (
    <div onClick={div1Handler}>
      <div onClick={div2Handler}>
        <button onClick={buttonHandler}>Клик</button>
      </div>
    </div>
  );
}

Code Example 3: Modal closing patterns

❓ В чём разница между этими двумя способами? Какой предпочтительнее и почему?

stopPropagation:

tsx
function Modal({ onClose, children }) {
  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>
  );
}

Target check:

tsx
function Modal({ onClose, children }) {
  const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
    if (e.target === e.currentTarget) {
      onClose();
    }
  };
 
  return (
    <div className="overlay" onClick={handleOverlayClick}>
      <div className="modal-content">
        {children}
      </div>
    </div>
  );
}

Code Example 4: Card with action button

❓ Зачем здесь stopPropagation? Что произойдёт при клике на "В корзину" без него?

tsx
function ProductCard({ product, onSelect, onAddToCart }) {
  const handleCardClick = () => {
    onSelect(product.id);
  };
 
  const handleAddClick = (e: React.MouseEvent) => {
    e.stopPropagation();
    onAddToCart(product.id);
  };
 
  return (
    <div className="card" onClick={handleCardClick}>
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <p>{product.price} ₽</p>
      <button onClick={handleAddClick}>В корзину</button>
    </div>
  );
}

Code Example 5: Dropdown with click outside

❓ Как работает закрытие dropdown при клике вне его? Зачем проверять contains()?

tsx
function Dropdown({ trigger, children }) {
  const [isOpen, setIsOpen] = useState(false);
  const dropdownRef = useRef<HTMLDivElement>(null);
 
  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
        setIsOpen(false);
      }
    };
 
    if (isOpen) {
      document.addEventListener('click', handleClickOutside);
    }
 
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, [isOpen]);
 
  return (
    <div ref={dropdownRef}>
      <button onClick={() => setIsOpen(!isOpen)}>{trigger}</button>
      {isOpen && <div className="dropdown-menu">{children}</div>}
    </div>
  );
}

Code Example 6: Event delegation with closest

❓ Как работает closest() в этом примере? Зачем он нужен вместо проверки tagName?

tsx
function ItemList({ items }: { items: Item[] }) {
  const handleClick = (e: React.MouseEvent<HTMLUListElement>) => {
    const target = e.target as HTMLElement;
    const li = target.closest('li');
 
    if (!li) return;
 
    const id = li.dataset.id;
    const action = target.dataset.action;
 
    switch (action) {
      case 'edit':
        console.log('Редактировать:', id);
        break;
      case 'delete':
        console.log('Удалить:', id);
        break;
      default:
        console.log('Выбрать:', id);
    }
  };
 
  return (
    <ul onClick={handleClick}>
      {items.map(item => (
        <li key={item.id} data-id={item.id}>
          <span>{item.name}</span>
          <button data-action="edit">✏️</button>
          <button data-action="delete">🗑️</button>
        </li>
      ))}
    </ul>
  );
}

Code Example 7: Portal event bubbling

❓ Почему клик по кнопке в портале вызовет "Parent clicked"? Ведь в DOM кнопка находится в другом месте?

tsx
function Parent() {
  const handleClick = () => console.log('Parent clicked');
 
  return (
    <div onClick={handleClick}>
      <PortalChild />
    </div>
  );
}
 
function PortalChild() {
  return ReactDOM.createPortal(
    <button>Кнопка в портале</button>,
    document.getElementById('portal-root')!
  );
}

Code Example 8: stopPropagation anti-pattern

❓ Почему Version A считается антипаттерном? Чем лучше Version B?

Version A:

tsx
function BadComponent() {
  return (
    <div onClick={(e) => { e.stopPropagation(); doSomething(); }}>
      <span onClick={(e) => { e.stopPropagation(); doOther(); }}>
        <button onClick={(e) => { e.stopPropagation(); doAction(); }}>
          Клик
        </button>
      </span>
    </div>
  );
}

Version B:

tsx
function GoodComponent() {
  return (
    <div onClick={handleContainer}>
      <button onClick={(e) => { e.stopPropagation(); handleAction(); }}>
        Действие
      </button>
    </div>
  );
}