useEffect как синхронизация, зависимости, cleanup, refs, useEffectEvent, race conditions и отмена работы.
Effect нужен не после каждого render, а только когда React-состояние синхронизируется с системой вне React.
Вы удалите лишние Effects, напишете симметричные subscriptions, устраните stale closure и race conditions и примените refs как escape hatch.
До начала достаточно render/commit, state snapshots, closures и async cancellation. В конце должен появиться не конспект, а виджет живого соединения с корректным lifecycle, отменой, latest non-reactive callbacks и фокусом.
Effect выполняется после commit и приводит внешний ресурс в соответствие текущим props/state: соединение, browser API, widget, subscription. Если значение можно вычислить во время render или действие вызвано handler, Effect не нужен. Формулируйте ресурс и симметричный cleanup до написания кода.
Рабочая проверка. Для каждого Effect назовите внешнюю систему; удалите Effect без такой системы.
Типичная поломка. Пересчитывать fullName из firstName/lastName через Effect и state.
Effect возвращает cleanup, который отменяет именно сделанный setup: remove listener той же функцией, unsubscribe, disconnect, clear timer. Перед новым setup с изменёнными deps React сначала выполняет прежний cleanup; при unmount — последний cleanup. Эта симметрия делает remount безопасным.
Рабочая проверка. Проверьте количество listeners после смены roomId и unmount.
Типичная поломка. Передать removeEventListener новую arrow, не совпадающую с добавленной.
Все reactive values, прочитанные Effect, входят в deps. Список не является ручной политикой запуска; он следует коду. Чтобы изменить deps, измените code structure: перенесите object внутрь Effect, вынесите константу из компонента, используйте updater или Effect Event. Подавление lint создаёт stale closure.
Рабочая проверка. Исправьте Effect с object dependency без JSON.stringify и suppression.
Типичная поломка. Оставить пустой массив, хотя Effect читает меняющийся roomId.
Несколько async операций могут завершиться не по порядку. Cleanup помечает результат устаревшим или abort-ит поддерживаемый fetch. Abort не всегда отменяет серверную работу и может прийти после завершения, поэтому commit результата всё равно связывают с текущим запросом. Ошибка отмены обычно не показывается как сбой пользователю.
Рабочая проверка. Искусственно задержите старый запрос дольше нового и проверьте итог.
Типичная поломка. Применять любой последний завершившийся ответ независимо от входа Effect.
Ref сохраняет объект между renders и не вызывает render при записи. Подходит для DOM handle, timer id, latest imperative instance и данных, которые не влияют на JSX. Не читайте и не пишите ref.current во время render, кроме предсказуемой ленивой инициализации; это нарушает чистоту.
Рабочая проверка. Сохраните timer id в ref, очистите его при повторном запуске и unmount.
Типичная поломка. Хранить status, который отображается пользователю, только в ref.
useLayoutEffect и layoutuseLayoutEffect запускает setup после DOM mutations, но до paint, и может синхронно вызвать render. Он нужен для измерения layout и корректировки, которую пользователь не должен увидеть промежуточной. Блокирует paint, поэтому network, logging и обычные subscriptions остаются в useEffect. На сервере layout отсутствует.
Рабочая проверка. Измерьте tooltip и разместите его до paint, затем сравните с обычным Effect.
Типичная поломка. Заменить все Effects на layout Effects «для надёжности».
useEffectEvent для non-reactive чтенияEffect Event позволяет Effect вызвать логику, которая всегда читает latest props/state, но не должна сама делать Effect реактивным. Например, соединение зависит от roomId, а уведомление использует текущую theme. Effect Event вызывают только из Effects и не передают как общий callback. Это не способ скрыть настоящую dependency.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
const onConnected = useEffectEvent(() => {
showNotification('Подключено', theme);
});
useEffect(() => {
const connection = connect(roomId);
connection.on('connected', onConnected);
return () => connection.close();
}, [roomId]);Смена theme меняет вид уведомления, но не является причиной переподключения к комнате.
Рабочая проверка. Смените theme без reconnect и проверьте latest theme при следующем connected.
Типичная поломка. Убрать roomId из deps, спрятав его чтение в Effect Event.
Цепочка Effect A → setState → Effect B → setState создаёт лишние commits и хрупкие промежуточные состояния. Событие пользователя может вычислить все переходы одним handler/reducer. Derived данные вычисляются при render. Effect review классифицирует каждый Effect: external subscription, imperative widget, telemetry view event или кандидат на удаление.
Рабочая проверка. Сверните каскад выбора карточки и сброса round в одно event transition.
Типичная поломка. Координировать бизнес-workflow наблюдением за state после commit.
Исправьте чат: соединение пересоздаётся на смену theme, старый fetch перезаписывает новый, listener течёт, а ref читается в render. Примените Effect, cleanup, AbortController и useEffectEvent по контракту.
Критерий завершения: StrictMode setup-cleanup-setup оставляет один ресурс, устаревший ответ не коммитится, зависимости проходят lint без suppression. Сначала зафиксируйте наблюдаемое поведение тестом или измерением, затем меняйте реализацию.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Пользовательские хуки