Галоўная / Артыкулы

Аудыт функциі useEffect: дзесяць распашчых проблем, іх сімптамы і рэальныя способы адраджэння

Адзначыце дзесяць найпаўзлучэйшых адказакоў функцыі useEffect у React — ад неяконкрэтнага чыставання і застарелых клаўзураў да супернікаючых умов і сінхронізацыі пропаў — і застосавайце правильныя спасабы выправлення.

Выдавец: REACTAPP.TOP8 хв чытання1919 слоў

Крыніцы: 10 React useEffect Mistakes That Are Secretly Breaking Your App in 2026

React 19, React Compiler і Server Components перанеслі многа завдання з useEffect, пры тым хук яшчэ залишаецца месцам, дзе з’яўляюцца большасць самых складных багоў у React: вытэкі дадзеных, застарелыя данні, мерцанне і цыклы рэндараў. Большасць з іх выпадае з-за дзесяці часта павтараючыхся помылак. Ёсць опис бракавальнай версіі, сімптамаў і способу выправлення кожнай з іх, а таксама чарткі для перагляду своіх эфектаў.

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

1. Забыванне пра функцыю чысткі

Таймеры, падпісанні і аберанты запуску зместу, створаныя ў эфекте, продовжаюць працаваць пасля таго, як компанент зноў рэндаруецца або ад’ёмляецца, якщо толькі іх не зняць. У гэтым прыкладзе інтэрвал ніколі не чыстаецца:

// ❌ Broken: No cleanup
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  // Missing: return () => clearInterval(timer);
}, []);

Сімптамы – это утэклівыя інтэрвалы та дуплікацыя слухачоў. У режыме разработкі Strict Mode спецыяльна падключае, ад’ўязывае та зноў падключае компаненты, ўбачліваючы гэта: без чысткі вы отрываеце два інтэрвалы, і лічылка працюе два разы на секунду. У продакшэне утэк выявляецца ў кожны момент, калі компанент зноў падключаецца. Паруцьце функцыю, якая скасоўвае настройкі:

// ✅ Fixed: Always clean up
useEffect(() => {
  const timer = setInterval(() => {
    setCount(c => c + 1);
  }, 1000);
  return () => clearInterval(timer);
}, []);

2. Іспытанне пустага массива залежнасцяў для значэння „з’ядзіць раз“

Пусты массив не означае „пад час падключэння“ ў якім-лібо спецыяльным сэнсе. Ён паведамляе React, што эфект не залежыць нічага, таму ён ніколі не будзе запускацца занова. Якщо эфект чытае пропсы або стан, гэтыя значэнні застаюцца такімі ж, якімі былі пад час першага атрыбутавання:

// ❌ Broken: Stale closure
function ChatRoom({ roomId }) {
  useEffect(() => {
    connectToRoom(roomId);
    return () => disconnectFromRoom(roomId);
  }, []); // roomId changes, but effect never re-runs
}

Калі змінюецца roomId, компанента застаецца ў первоначальной камнате. Указайце всі рэактыўныя значэнні, якія викорыстоўваецца ў эфекте; правіла лінтингу react-hooks/exhaustive-deps выяўляюць гэта.

// ✅ Fixed: Include real dependencies
useEffect(() => {
  connectToRoom(roomId);
  return () => disconnectFromRoom(roomId);
}, [roomId]);

Тепер React раз’яўляе звязак з пылей камнатай і паўтарна налажвае звязак з новай кожны раз, калі змінюецца roomId.

3. Залежнасці ад об’ектаў, якія перзаснавляюцца ў кожны раз пад час рэндара

Залежнасці порৱнююцца па рэферэнсе. Об’ект або масіўны літерал, створаны пад час рэндара, ў кожны раз являе сабою новы рэферэнс, таму эфект, які ад ньго залежыць, паўтарна запускаецца пасля кожнага рэндара:

// ❌ Broken: New object every render
function Parent() {
  const config = { timeout: 5000 }; // New reference every time

  useEffect(() => {
    fetchData(config);
  }, [config]); // Runs every single render
}

Стабілізавайце рэферэнс. Якщо значэнне ніколі не змянюецца, задаўце яго аднойчы за межамі компонента. Якщо яно ствараецца з пропсаў або стата, загорніце яго ў useMemo з правымі залежнасцямі. Нижэйшы фрагмент паказвае оба варыянты падольгу; на практыцы трэба выбраць адны:

// ✅ Fixed: Stable reference
const CONFIG = { timeout: 5000 }; // Defined outside component
function Parent() {
  const config = useMemo(() => ({ timeout: 5000 }), []);

  useEffect(() => {
    fetchData(config);
  }, [config]);
}

Часта ўсё простае: выкорыстоўвайце прымітывы (timeout замест config) і стварайце об’ект унутры эфекта. Кампілятор React мемуець багато значэнняў у кодзе, які ён кампілюе, але не об’екты, якія прыходзяць з некампільванага коду або бібліятэкаў, таму не пакладайцеся на яго для правільнасці эфектаў.

4. Дазволэнне асінхронным адпаведзям супернаць одно другаму

Калі залежнасць змянюецца быстра, як у выпадку польца пошуку або шыранага переключэння віконцаў, можа адразу быць калька запытак, і яны не павінны рашацца па порядку:

// ❌ Broken: Race condition
useEffect(() => {
  fetch(`/api/search?q=${query}`).then(res => res.json()).then(setResults);
}, [query]);

Якщо корыстнік увводзіць „react“, а пасля „react hooks“, адказ на запит працэўкі „react“ можа прыйсці апошнім і перазапісаць правильныя рэзультаты. Рашэння — анулюваць застарелы запит пад час очішчэння. AbortController анулюе запуск запиту, а выйшоўшы AbortError ігнаруецца, таму ён не фіксуецца як справжняя памылка:

// ✅ Fixed: Abort in-flight requests
useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/search?q=${query}`, { signal: controller.signal })
    .then(res => res.json())
    .then(setResults)
    .catch(err => {
      if (err.name !== 'AbortError') setError(err);
    });

  return () => controller.abort();
}, [query]);

Для інтерфейсаў пошуку є ўжо большая колькасць тонкасцей, пра якія пішаўся у способах усунення ситуацый канкурэнціі, якія не можна рашыць за дапамогою тэхнік debouncing.

5. Зберагчыце вырахаваныя значэнні ў стацы

Якщо значэнне можна вырахаваць з існуючых пропсоў або стацы, яму не патрэбна сасвойная стацы чы эфект:

// ❌ Broken: Redundant state + effect
const [fullName, setFullName] = useState('');
useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

Кожная змена атрыбута практыкуецца два разы: спачатку з застарэлым значэнням fullName, і тады утвараюцца два аднаковых источнікі даных. Значэнне трэба вычысляць пад час атрыбутавання, вядучы useMemo толькі для справаў, якія даскладны ў вычысленні:

// ✅ Fixed: Compute during render
const fullName = `${firstName} ${lastName}`;// Or for expensive computations
const fullName = useMemo(() => `${firstName} ${lastName}`, [firstName, lastName]);

Заўважыце, што у прыкладзе паказаны два варыянты; выказванне fullName два разы ў аднам і тым жа пространстве імен ёсць синтаксічныя памылкай, таму трэба застаўіць толькі адны. Болей шырокі патэран розглядаецца ў безпечнейшай альтэрнатыве сінхронізацыі стану з useEffect.

6. Стварэнне цыклу апдэйта

Эфект, які выставляе стан, таксама вечна залежыць ад самага графіка своіх выконанняў:

// ❌ Broken: Infinite loop
useEffect(() => {
  setCount(count + 1);
}, [count]);

Апдэйт спрыяе перрадзіць элемент, перрадзіцель зменшае значэнне count, зменавана гульня count зноў запускае эфект, і так далей. React ў канцы канцоў зупіняе гэты цикл за дапамой падказкі „Значэнне максымальной глыбі апдэйта пераканана“. Функцыйнае апдэйт усуняе патрэбу чытаць count унутрані эфекта, таму яго больш не трэба выклікаць як залежнасць:

// ✅ Fixed: Functional update + correct dependencies
useEffect(() => {
  const timer = setTimeout(() => {
    setCount(c => c + 1); // Functional update avoids dependency
  }, 1000);
  return () => clearTimeout(timer);
}, []); // No count dependency needed

Заўважыце, што гэта зміняе працэс: лічылка тепер адрастае адно раз, час праз два пасля стварэння элемента. Для лічылкі, якая адрастае пастойна, наладзіце setInterval з функцыяй чысткі, як у прыкладзе 1. Кожны вызов setState у эфекте патрэбуе умовы, якая гарантуеўвае його завершэнне.

7. Полное выключэнне массива залежнасцяў

Без другога аргумента эфект запускаецца пасля кожнага перрадзення элемента. Іноды гэта і є бажаным, але зазвычай гэта вялікі брак і даскладнае рашэння:

// ❌ Broken: Runs on every render
useEffect(() => {
  console.log('Effect running');
  performHeavyComputation(data);
});

Кампалія React скаржы непатрэбныя павтаральныя адраджыванні, ал яна не робіць працу ў вашых эфектах дышэўней, таму неконтрольваны эфект, які выкаанаў важку працу, застаецца бутылкавым горлам. Заявіце рэальную залежнасць:

// ✅ Fixed: Explicit dependencies
useEffect(() => {
  performHeavyComputation(data);
}, [data]);

8. Адрабатка падзеяў корыстніка ў эфектах

Эфекты запускаюцца тады, калі ўсё адраджваецца, а не тады, калі корыстнік чагосьці зрабіў. Аналітыка, прыязначаная да флага статаусу, запускаецца ўсё час, калі гэты флаг змінюецца, незалежна ад прычыны:

// ❌ Broken: Event logic in effect
useEffect(() => {
  if (isModalOpen) {
    trackEvent('modal_opened');
  }
}, [isModalOpen]);

Гэтая падзея фіксуецца пад час запуску, якщо модальны элемент відкрываецца, калі родны элемент зменяе свойства, або калі статаус восстанавліваецца, а не толькі калі корыстнік нажмае на кнопку. Логіку взаімадзеяў пакладзіце ў функцыю-обработчык, якая выканае гэтую взаімадзей:

// ✅ Fixed: Event handler
function handleOpenModal() {
  setIsModalOpen(true);
  trackEvent('modal_opened');
}

Якщо вы можете назваць дзеянне корыстніка, якое павінна запусціць код, яно патрабуе месца ў функцыі-обработчыку.

9. Зялучэнне дадзеных за дапамою простага эфекта

У багацькох дапліках, дзе компаненты сервера і загрузчыкі фреймворка адмаўляюць пачатковыя даны, запрашэння з боку кліента ў эфектах становяцца менш неабходнымі. Калі вы працуеце з запрашэннямі на боку кліента, просты эфект є нестабільным:

// ❌ Broken: Raw fetch in effect
useEffect(() => {
  fetch('/api/user').then(r => r.json()).then(setUser);
}, []);

У яго няма статусу загрузкі, няма обробкі адзінакоў, няма унікалізацыі запрашэнняў, няма кэшавання, і, як паказвае глытка 4, няма захадоў проты супернагонкі. Бібліятэка для запрашэння даных адмаўляе все гэта:

// ✅ Fixed: Use a data-fetching library
const { data: user, isLoading, error } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  staleTime: 5 * 60 * 1000,
});

TanStack Query і SWR — это стандартныя выборы. Без іх трэба напісаць адзін спецыяльны хук з обробкай статусу загрузкі, адзінакоў і абрання запрашэння, замест таго каб викорыстоўваць спецыяльныя эфекты запрашэння.

10. Копіюванне пропсаў у статус і синхронізацыя іх

Компанент, які прымеў пачатковую значэння, копіюе яе у статус і пасля таго викорыстоўвае эфект, каб адбіць змяны пропсаў, фактычна борэць з самым сабой:

// ❌ Broken: Prop-to-state sync
function EmailInput({ initialEmail }) {
  const [email, setEmail] = useState(initialEmail);

  useEffect(() => {
    setEmail(initialEmail); // Sync prop to state
  }, [initialEmail]);

  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}

Кожны раз, калі вышэйшы элемент задае новую значенне initialEmail, данні, які запісвае корыстувальнік, таямніча заменяюцца на новыя пасля додатковага атрыбутавання старым значэннем. Выберыце адзін падход: зробіце компонент цалком керованым, альбо залишыце яго некерованым і спецыяльна скасавайце його стан, зменіўшы значэнне key, чыяму React дае новы экземпляр з наўвесным станам:

// ✅ Fixed: Controlled or fully uncontrolled
// Option 1: Fully controlled — no internal state
function EmailInput({ email, onChange }) {
  return <input value={email} onChange={onChange} />;
}
// Option 2: Fully uncontrolled with key
function EmailInput({ initialEmail }) {
  const [email, setEmail] = useState(initialEmail);
  return <input value={email} onChange={e => setEmail(e.target.value)} />;
}
// Usage: Reset by changing key
<EmailInput key={userId} initialEmail={user.email} />

Як React 19 змініў ситуацыю

Нявечныя версіі React робяць такія прычынкі яшчэ болей виднымі і менш неабходнымі:

  • Режым строгай перацэлавання, які дазволяе двойную інсталляцыю эфектаў толькі пад час розрабоцы, выявляе відсутныя або некоректныя элементы чысткі ў раннім періоде.
  • Кампайлер React автаматычна запам’ятоввае компоненты і багато значэнняў, чым зменшае випадковую пераатрыбутавання, але не выправляе логіку эфектаў.
  • Компаненты сервера абсалютна большая частка пачатковага завантажэння дадзейнаў, таму эфекты выкарыстоўваюцца пераважна толькі для роботы на боку кліента, такой як падпісанні, анімазыі і API прыгледача.
  • API use можа чытаць праграмы і контэкст пад час рэндара, чым усунуліся дзеяныя прычыны для выкарыстоўвання эфектаў для асінхронных значэнняй.
  • Спіс пераконтрацоў прыямо перед з’еднаннем для эфектаў

    Перад тым, як эфект будзе выпускацца, пераканайцеся:

    • Кожна падпіска, таймер і аберанець маюць відпаведную процэдуру чысткі.
    • Масавы залежнасцей указаныя ўсе рэактыўныя значэнні, якія чытае эфект.
    • Залежнасці у вачынку і масавых структурах є стабільныя, або замененыя на прымітныя залежнасці.
    • Асінхронная робота анулюецца або ігнаруецца, калі змянююцыся залежнасці.
    • Нічога, што выраховваецца ў эфекте, не можа быць проста выведзена пад час рэндара.
    • Жодная зміна стану не можа паўтарна запусціць той самы эфект без умовы абаранення.
    • Логіка, яка запускаецца па дзействах пользователя, знаходзіцца ў обробніках запуску падзеяў.
    • За можлівасцю для запошчання дадзэнняў выкорыстоўваецца компанента сервера, загрузчык фрэймворку або бібліятэка для запошчання дадзэнняў.
    • Кожны элемент стану мае толькі аднаго адпаведнага джерела.

    Узагадкі

    Паўнае большасць бягоў у useEffect вынікае з выкорыстанняя гука для чаго-небудзь другога, кроме зовнішняй сінхронізацыі. Працэс вычыслення похадных значэнняў атрыбутуецца часткі коду, якая відпаведае за атрыбутыванне, дзейства пользователя — обробнікам падзеяў, дадзеныя з сервера — шару дадзэнняў, а стан, які можна перазначыць, — элементу key. Асалодныя падпіскі, якія застаюцца, выкалічваюцься на адпаведных залежнасцях і патрабуюць правільнага ачысцэння. Часта найлепым рэзультатам аудыты ёст тое, што такі эфект проста выдаліваецца.