Ефекти
Накратко: Ефектът е код за страничните действия (таймер, заявка, заглавие на раздела), който useEffect пуска след рисуването и почиства преди следващото пускане и при махане.
Проблем
Банерът си отиде, проверките остават
Баницата със сирене е изчерпана. Купувачът натиска „Следи наличността“ и под бутона се появява банер. Докато банерът е отворен, страницата проверява наличността на всяка секунда. Тук проверката само пише ред в конзолата.
Конзола
Натисни „Следи наличността“, изчакай две секунди и натисни „Спри следенето“. Банерът изчезва, а в конзолата пак идват „Проверка №3“, „Проверка №4“… Отвори и затвори банера още веднъж: сега проверките са два реда на секунда. Таймерът е на браузъра, а не на компонента. Никой не го е спрял.
Идеята
Рисуването само изчислява
Компонентът казва какво да има на екрана: от props и състоянието изчислява разметката. Това е рисуването. Библиотеката го повтаря, когато поиска, и сравнява резултата с предишния.
Затова рисуването трябва да е чисто: да не прави нищо друго, освен да изчисли разметката. Таймерът, заявката към сървъра, смяната на document.title, абонаментът за събития на window правят нещо навън. Те са странични ефекти (side effects).
Тялото на функцията StockWatcher е рисуването. То върви при всяко рисуване, а в примера по-горе пуска нов таймер всеки път. Тук не личи, защото банерът се рисува веднъж. Но ако таймерът сменя състояние (например брояч на проверките на екрана), всяка смяна рисува наново, а всяко рисуване пуска още един таймер. Броят им расте с всяка секунда.
Страничният ефект има нужда от две неща, които рисуването не дава: момент, в който да тръгне, и момент, в който да спре.
Идеята
Ефект и почистване
Ефектът е код, който компонентът пуска, след като е на страницата. Почистването е кодът, който спира пуснатото, когато компонентът си отиде.
import { useEffect } from 'react';
export default function StockWatcher({ product }) { useEffect(() => { const id = setInterval(check, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Следим наличността на {product.name}.</p>;}useEffect(функция, зависимости)казва на React: „пусни тази функция, след като промените са в DOM-а“.- Функцията, която ефектът връща, е почистването. React я вика, когато компонентът излезе от страницата.
- Вторият аргумент е списъкът със зависимости: стойностите, които ефектът чете. За него е следващата идея.
Почистването е огледало на ефекта: setInterval срещу clearInterval, addEventListener срещу removeEventListener, заявка срещу отказ на заявката.
Пробвай
Спри проверките
Таймерът в StockWatcher тръгва направо в компонента и никой не го спира. Премести го в ефект, който тръгва, когато банерът се появи, и го спри, когато банерът си отиде. Проверката отваря банера, чака „Проверка №1“, затваря го и гледа, че „Проверка №2“ не идва.
Конзола
Решение
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCT = { id: 4, name: 'Баница със сирене' };
export default function App() {
const [watching, setWatching] = useState(false);
return (
<main>
<h1>{PRODUCT.name}</h1>
<p>Изчерпан. Ще се появи отново скоро.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Спри следенето' : 'Следи наличността'}
</button>
{watching && <StockWatcher product={PRODUCT} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Проверка №${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Следим наличността на {product.name}. Ще ти кажем, щом се появи.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
Таймерът отива в useEffect. Ефектът връща () => clearInterval(id), а зависимостите са [product]. Проверката хваща ефект без return: таймерът пак остава. Хваща и return clearInterval(id); без стрелката: тогава clearInterval се вика веднага, таймерът спира, преди да провери веднъж, а React получава undefined вместо функция.
Идеята
Зависимостите
Сега купувачът може да избере друг изчерпан продукт, докато банерът е отворен. Ефектът проверява product. Щом продуктът се смени, старата проверка трябва да спре, а нова да тръгне за новия продукт.
Затова е списъкът със зависимости. След всяко рисуване React сравнява всяка стойност в него с миналия път. Ако поне една е различна, първо вика почистването на старото пускане, после пуска ефекта наново.
| Вторият аргумент | Кога върви ефектът |
|---|---|
[product] |
след първото рисуване и след всяко, при което product е различен |
[] |
само след първото рисуване |
| без него | след всяко рисуване |
Правилото: в списъка е всяка стойност от props и състоянието, която ефектът чете. Ако product липсва, ефектът остава със стария продукт. Функцията на ефекта е създадена при първото рисуване и помни стойностите от тогава.
Виж го
Виж живота на един ефект
Банерът се отваря за баницата, купувачът избира краставиците, после затваря банера.
function StockWatcher({ product }) { useEffect(() => { let checks = 0; const id = setInterval(() => { checks++; console.log(`Проверка №${checks}: ${product.name}`); }, 1000); return () => clearInterval(id); }, [product]);
return <p role="status">Следим наличността на {product.name}</p>;}Стъпка по стъпка, с думи
- Рисуване: React вика StockWatcher и получава разметката. Функцията само изчислява какво да се покаже. Не пуска нищо.
- Компонентът е на страницата. Сега React пуска ефекта: таймерът тръгва и брои проверките.
- Купувачът избира друг продукт. Prop-ът се сменя, StockWatcher се рисува наново и React сравнява [product] с миналия път: различен е.
- Преди новото пускане идва почистването на старото: неговият таймер спира. Без него старите проверки щяха да вървят до новите.
- Ефектът тръгва наново с новия продукт. Неговият таймер брои от №1.
- Купувачът затваря банера: компонентът излиза от страницата. Върви последното почистване и таймерът спира с него.
- Всяко пускане има своя двойка: каквото ефектът пусне, почистването му го спира. Преди следващото пускане и когато компонентът си отиде.
Ефектът тръгва два пъти и почистването върви два пъти: веднъж преди новото пускане, веднъж при махането. Между тях винаги работи точно един таймер.
Пробвай
Намери грешката
Избери реда с грешката.
[] значи „само след първото рисуване“. Ефектът не тръгва наново за новия продукт, а функцията му помни стария. Трябва [product].Пробвай
Попълни
Какво стои на празното място, за да спре таймерът, когато компонентът си отиде?
useEffect(() => {
const id = setInterval(check, 1000);
() => clearInterval(id);
}, [product]);return: функцията, която ефектът връща, е почистването. React я вика преди следващото пускане и при махането на компонента.Идеята
Кога не ти трябва ефект
Ефектът е за връзка с нещо извън компонента: таймер, сървър, заглавието на раздела, window. Две неща изглеждат като ефект, но не са.
- Стойност, изчислена от други. Общата сума на кошницата не се пази в отделно състояние, което ефект обновява. Тя се изчислява при рисуване (или с
useMemo), както в урока за изчисленото състояние. Ефект, който я пише в отделно състояние, е излишна работа и второ място, което може да се размине с кошницата. - Отговор на действие. Поръчката се праща, защото купувачът е натиснал „Поръчай“. Това е в обработчика на клика, а не в ефект, който гледа някакъв флаг.
Остава истинската работа на ефекта: да синхронизира компонента с външния свят, докато компонентът е на страницата.
По-точно, за любопитните
Двойното пускане в разработка. В нов проект с Vite main.jsx обвива <App /> в <StrictMode>. В режим на разработка StrictMode нарочно пуска всеки ефект, почиства го и го пуска пак, веднага при показване. Ако в конзолата видиш „ефектът тръгна“ два пъти, това е проверка, а не грешка: ефект с правилно почистване издържа на нея. Пясъчникът не ползва StrictMode, затова тук ефектът тръгва веднъж.
Защо не useEffect(async () => …). Async функция връща обещание, а React очаква функцията за почистване или нищо. Конзолата казва useEffect must not return anything besides a function, which is used for clean-up. Async функцията се пише вътре в ефекта и се вика там. Това е в следващия урок.
Как се сравняват зависимостите. С Object.is, като при състоянието. Обект, създаден наново при всяко рисуване, е различен всеки път и ефектът тръгва всеки път. Правилото „всичко, което ефектът чете, е в списъка“ пази линтерът (react-hooks/exhaustive-deps), включен в шаблона на Vite.
Класовите компоненти имат същото в три метода: componentDidMount, componentDidUpdate и componentWillUnmount.
Упражнение
Друг продукт
Над банера има падащо меню „Продукт“ с баницата и краставиците. Сега StockWatcher пуска таймера веднъж, при показване. Направи така, че при смяна на продукта старата проверка да спре, а нова да тръгне от №1 за новия продукт. Проверката отваря банера, чака „Проверка №1“ за баницата, избира краставиците, чака „Проверка №1“ за тях, гледа, че „Проверка №2“ за баницата не идва, и затваря банера.
Конзола
Решение
App.jsximport { useState } from 'react';
import StockWatcher from './StockWatcher.jsx';
const PRODUCTS = [
{ id: 4, name: 'Баница със сирене' },
{ id: 13, name: 'Краставици' },
];
export default function App() {
const [productId, setProductId] = useState(4);
const [watching, setWatching] = useState(false);
const product = PRODUCTS.find((p) => p.id === productId);
return (
<main>
<label>
Продукт{' '}
<select value={productId} onChange={(e) => setProductId(Number(e.target.value))}>
{PRODUCTS.map((p) => (
<option key={p.id} value={p.id}>{p.name}</option>
))}
</select>
</label>
<h1>{product.name}</h1>
<p>Изчерпан. Ще се появи отново скоро.</p>
<button type="button" onClick={() => setWatching(!watching)}>
{watching ? 'Спри следенето' : 'Следи наличността'}
</button>
{watching && <StockWatcher product={product} />}
</main>
);
}
StockWatcher.jsximport { useEffect } from 'react';
export default function StockWatcher({ product }) {
useEffect(() => {
let checks = 0;
const id = setInterval(() => {
checks++;
console.log(`Проверка №${checks}: ${product.name}`);
}, 1000);
return () => clearInterval(id);
}, [product]);
return (
<p className="watcher" role="status">
Следим наличността на {product.name}. Ще ти кажем, щом се появи.
</p>
);
}
style.cssbody { font-family: system-ui, sans-serif; margin: 0; padding: 0 16px; }
.watcher { background: #f3efe6; border-left: 4px solid #c27c2a; padding: 8px 12px; }
label { display: block; margin: 8px 0; }
button, select { font: inherit; }
Списъкът със зависимости става [product], а ефектът връща () => clearInterval(id). При смяна React първо вика почистването на стария таймер, после пуска ефекта с новия продукт. Проверката хваща [product] без почистване: тогава двата таймера вървят един до друг.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Props: данни отгоре · Условия: празно, грешка, зарежда · Състояние · Изчислено състояние
- Продължава в: Данни от API в компонент · Бързина: кога се рисува наново · Тестове на компоненти · От пясъчника към проект
- Задачи по темата: Ефекти, данни и формуляри