Почему useEffect часто ломает код

Очень много React-кода выглядит так: const [fullName, setFullName] = useState(‘’);

useEffect(() => {   setFullName(${user.name} ${user.lastName}\); }, [user]);

Хотя здесь useEffect вообще не нужен.

Одна из самых частых проблем в React - использование useEffect как места “для любой логики”. В него начинают складывать вычисления, derived state, фильтрацию, обработку данных, синхронизацию state и тд. Хотя изначально useEffect нужен совсем для другого.

Главная идея useEffect: синхронизация React с внешним миром. То есть с тем, что находится за пределами React.

Например: - запросы к API - подписки - WebSocket - DOM API - таймеры - localStorage

Если что-то можно вычислить во время рендера, useEffect чаще всего не нужен.

Вместо этого: const [fullName, setFullName] = useState(‘’);

useEffect(() => {   setFullName(${user.name} ${user.lastName}\); }, [user]);

лучше написать: const fullName = ${user.name} ${user.lastName};

Но даже когда useEffect действительно нужен, многие забывают про cleanup-функции.

Например: useEffect(() => {   window.addEventListener(‘resize’, handleResize); }, []);

На первый взгляд всё нормально. Но проблема в том, что обработчик никогда не удалится. Если компонент размонтируется, listener останется в памяти.

Правильный вариант: useEffect(() => {   window.addEventListener(‘resize’, handleResize);

return () => {     window.removeEventListener(‘resize’, handleResize);   }; }, []);

То же самое касается таймеров, подписок, WebSocket, observer’ов и тд и тп

Проблема лишних или неправильных useEffect не только в “красоте кода”.

Они часто приводят к: - лишним рендерам; - сложным зависимостям; - stale closure; - утечкам памяти; - бесконечным циклам; - трудноуловимым багам.

Очень хороший вопрос перед написанием useEffect: “Я синхронизирую React с чем-то внешним?” Если ответ: “нет, я просто вычисляю данные” то useEffect скорее всего не нужен.

Чем меньше useEffect в приложении тем обычно проще поддержка и меньше неожиданных багов.