Функциональные компоненты React представляют собой чистые функции рендеринга. Однако реальным веб-приложениям необходимо взаимодействовать с внешним императивным миром: обращаться к браузерному DOM (управление фокусом, скролл, замер геометрии), подключать веб-сокеты, подписываться на глобальные события окна и запускать таймеры.
Для выхода за рамки чистого декларативного рендеринга в React предусмотрены два ключевых хука:
useRef: механизм сохранения мутабельного значения между рендерами без инициирования повторной перерисовки UI. Чаще всего используется как прямой мост к нативным DOM-узлам браузера.useEffect: механизм декларативной синхронизации компонента с внешними системами (Side Effects). Эффект выполняется после того, как браузер завершил отрисовку кадра (Post-Paint), и гарантирует запуск функции очистки (Cleanup function) при демонтаже или изменении зависимостей.Важнейший архитектурный инвариант современного React: useEffect предназначен строго для синхронизации с внешним миром, а не для трансформации данных или вычисления зависимого состояния.
Взглянем на фундаментальную разницу между useState, useRef и useEffect:
+-------------------------------------------------------------------------+
| USESTATE vs USEREF vs USEEFFECT |
+-------------------------------------------------------------------------+
| useState: |
| Мутация стейта -> ПЛАНИРУЕТ РЕРЕНДЕР -> Экран обновляется |
| (Используется для данных, которые должны отображаться в JSX) |
+-------------------------------------------------------------------------+
| useRef: |
| Мутация ref.current = val -> РЕРЕНДЕР НЕ ВЫЗЫВАЕТСЯ! |
| (Используется для DOM-нод, таймеров id, предыдущих значений) |
+-------------------------------------------------------------------------+
| useEffect: |
| Вызывается ПОСЛЕ рендера и отрисовки DOM браузером (Commit -> Paint) |
| (Используется для сокетов, fetch, подписок document.addEventListener) |
+-------------------------------------------------------------------------+Компонент вычисляет JSX. Обращение к DOM или запуск тяжелых побочных эффектов здесь строго запрещены.
React мутирует реальное дерево DOM и обновляет ссылки ref.current.
Браузер выводит пиксели на экран. Пользователь видит актуальный интерфейс.
React вызывает тело эффекта, не блокируя отзывчивость интерфейса (INP менее 200 мс).
Перед повторным запуском или удалением компонента вызывается возвращенный деструктор.
Что произойдет с экраном, если внутри обработчика клика изменить значение ссылки: `countRef.current += 1`?
inputRef.current?.focus()).window.addEventListener('keydown', onKeyDown)).intervalId для очистки таймера при размонтировании.useRef#Для привязки ссылки к DOM-узлу передайте объект ref в проп ref любого нативного HTML-тега:
import React, { useRef, useEffect } from 'react';
export function AutoFocusSearch() {
// 1. Создание типизированной ссылки на HTMLInputElement
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
// 2. Доступ к DOM-узлу гарантирован ПОСЛЕ монтирования
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // Пустой массив зависимостей = строго один раз при монтировании
return (
<div className="p-4">
<input
ref={inputRef} // Привязка ссылки к DOM-ноде
type="search"
placeholder="Поиск по документации..."
className="px-4 py-2 border rounded-lg bg-bg-surface text-text-primary"
/>
</div>
);
}Каждый раз, когда вы создаете подписку в useEffect, вы обязаны вернуть функцию очистки (Cleanup) для предотвращения утечек памяти:
import React, { useState, useEffect } from 'react';
export function WindowResizeTracker() {
const [windowWidth, setWindowWidth] = useState(
typeof window !== 'undefined' ? window.innerWidth : 0
);
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
// 1. Подписка на внешнее событие
window.addEventListener('resize', handleResize);
// 2. ОБЯЗАТЕЛЬНАЯ очистка при демонтаже компонента
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Монтируем слушатель при старте, удаляем при уходе
return (
<div className="text-xs text-text-muted">
Ширина окна: {windowWidth}px
</div>
);
}Если компонент размонтировался до того, как сетевой запрос завершился, необходимо прервать сетевой сокет:
useEffect(() => {
const controller = new AbortController();
async function loadData() {
try {
const response = await fetch(`/api/user/${userId}`, {
signal: controller.signal, // Передаем сигнал отмены
});
const data = await response.json();
setUserData(data);
} catch (err: unknown) {
// Игнорируем ошибку отмены аборта
if (err instanceof Error && err.name === 'AbortError') return;
console.error('Ошибка загрузки:', err);
}
}
loadData();
// Отменяем сетевой запрос, если userId изменился или компонент закрылся!
return () => {
controller.abort();
};
}, [userId]);Какой из следующих сценариев является грубым антипаттерном использования useEffect?
Невозвращение функции очистки приводит к утечке памяти: при каждом ререндере создается новый слушатель, который накапливается в памяти браузера.
// ОШИБКА: утечка памяти!
useEffect(() => {
const id = setInterval(tick, 1000);
// Забыли return () => clearInterval(id);
}, []);Если не указать используемую переменную в массиве зависимостей, эффект «запомнит» ее значение из первого рендера навсегда:
// ОШИБКА: count навсегда равен 0 внутри таймера!
useEffect(() => {
const id = setInterval(() => {
console.log(count); // Запомнил count = 0
}, 1000);
return () => clearInterval(id);
}, []); // count не указан в зависимостях!Не читайте и не изменяйте ref.current в основном теле компонента. Тело рендера должно быть чистым! Изменяйте ref.current только внутри обработчиков событий или внутри useEffect.