Перестаньте постійно використовувати useEffect для кожної проблеми у React.
Ефекти синхронізуються з зовнішніми системами; генеруються під час рендерингу, обробляють кліки у подіях, отримуються за допомогою інструментів з реальними даними.
Пастка useEffect
Учні, які вивчають хуки, часто вдаються до useEffect для отримання даних, створення похідного стану та обробки натискань кнопок. Ефекти синхронізуються з зовнішніми системами — вони не є стандартним інструментом для звичайної логіки відображення.
Що насправді робить useEffect
Після малювання елементів React виконує ефекти для підключення слухачів, відкриття з’єднань чи безпосереднього взаємодіяння з чимось поза межами React. Неправильне використання призводить до мерехтіння, подвійного отримання даних та інших проблем.
// ❌ BAD: Two renders, unnecessary state
function UserProfile({ firstName, lastName }) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <h1>{fullName}</h1>;
}
// ✅ GOOD: Calculate during render
function UserProfile({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <h1>{fullName}</h1>;
}
// ❌ BAD: Unnecessary effect + state
function ProductList({ products }) {
const [sortedProducts, setSortedProducts] = useState([]);
useEffect(() => {
const sorted = [...products].sort((a, b) => a.price - b.price);
setSortedProducts(sorted);
}, [products]);
return (
<ul>
{sortedProducts.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
);
}
// ✅ GOOD: Calculate during render (useMemo for expensive ops)
function ProductList({ products }) {
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.price - b.price);
}, [products]);
return (
<ul>
{sortedProducts.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
);
}
// ❌ BAD: Manual fetching with useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (!cancelled) setUser(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId]);
if (loading) return <Spinner />;
if (error) return <p>Error: {error}</p>;
return <div>{user.name}</div>;
}
// ✅ GOOD: Use TanStack Query (React Query)
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isPending, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
if (isPending) return <Spinner />;
if (error) return <p>Error loading user</p>;
return <div>{user.name}</div>;
}
// ❌ BAD: Overcomplicating with useEffect
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
useEffect(() => {
if (liked) {
fetch(`/api/posts/${postId}/like`, { method: 'POST' });
} else {
fetch(`/api/posts/${postId}/unlike`, { method: 'POST' });
}
}, [liked, postId]);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
// ✅ GOOD: Handle in the event handler
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
const handleLike = async () => {
const newLiked = !liked;
setLiked(newLiked);
await fetch(`/api/posts/${postId}/${newLiked ? 'like' : 'unlike'}`, {
method: 'POST',
});
};
return (
<button onClick={handleLike}>
{liked ? '❤️' : '🤍'}
</button>
);
}
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup is crucial!
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>Window width: {width}px</p>;
}
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createWebSocketConnection(roomId);
connection.onMessage = (msg) => {
setMessages(prev => [...prev, msg]);
};
connection.connect();
return () => {
connection.disconnect(); // Cleanup!
};
}, [roomId]);
return <MessageList messages={messages} />;
}
function ChartComponent({ data }) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
if (chartRef.current && !chartInstance.current) {
chartInstance.current = new ChartLibrary(chartRef.current, {
type: 'line',
data: data,
});
}
return () => {
chartInstance.current?.destroy();
chartInstance.current = null;
};
}, []);
return <canvas ref={chartRef} />;
}
function PageTitle({ title }) {
useEffect(() => {
document.title = `${title} | My App`;
return () => {
document.title = 'My App'; // Reset on unmount
};
}, [title]);
return null;
}
Чотири поширені помилки
1. Обчислення значень у ефектах
Обчислюйте під час відображення (або за допомогою useMemo, коли це дороге). Ефекти, які змінюють setState на основі props/state, часто містять заборонені цикли зворотного зв’язку.
2. Дорогі обчислення в ефектах
Залишайте чисті обчислення у функціях відображення/memo, а не після малювання елементів.
3. Завантаження даних як неконтрольований ефект
Завантаження потребує можливості скасування, кешування та належного форматування повідомлень про помилки. Краще використовувати спеціалізовані бібліотеки чи фреймворки з механізмами керування процесом завантаження; якщо використовуєте ефекти, необхідно керувати ситуаціями переривання та конфліктів.
4. Дії користувача в ефектах
Обробники кліків мають знаходитися у функціях подій. Ефекти, які активуються через зміну статусного прапорця внаслідок кліку, створюють непрямість та помилки.
Дерево рішень
Якщо обчислення здійснюється на основі існуючого state/props → використовуйте функцію відображення. Якщо це реакція на клік/натискання „Надіслати“ → використовуйте обробник подій. Якщо це взаємодія з DOM API, мережевим джерелом чи життєвим циклом стороннього віджета → використовуйте ефект.
Коли ефекти є доцільними
Зовнішні слухачі подій
WebSockets
Ініціалізація бібліотек сторонніх розробників
Імперативні API, що не належать до React та які мають чисто завершувати свою роботу
Заключні зауваження
Комплексність менша, потік даних — чіткіший. Використовуйте useEffect лише для синхронізації з системами поза React, а не як універсальний інструмент для кожної операції після відображення контенту.
Режим Strict Mode подвійно запускає ефекти під час розробки, щоб виявити пропущені операції очищення — сприймайте це як перевагу під час роботи з сокетами. Для керування станом сервера використовуйте декларативні бібліотеки даних, щоб компоненти описували залежності, а не створювали власні ефекти отримання даних. Перед додаванням ефектів, які лише копіюють параметри до стану, проведіть аналіз — така практика зазвичай є неправильним розумінням процесу відображення контенту.
Функції очищення мають скасовувати слухачі та таймери; залишені без уваги обробники є типовою причиною попереджень про використання setState після зняття компонента.
Масиви залежностей є частиною контракту — якщо пропустити значення, з’являються застарілі функції; надмірне включення об’єктів та ефектів уповільнює кожне відображення.
Під час синхронізації з параметрами пошуку URL або зовнішніми сховищами краще використовувати спеціально створені хуки, які інкапсулюють межі дії ефектів.
Додатки, які генеруються на сервері, не повинні запускати ефекти, які працюють лише в браузері, без захисних механізмів; це потрібно контролювати за умовами середовища чи життєвим циклом.
Функції очищення мають видаляти слухачі та таймери; залишені без уваги обробники є класичною причиною попереджень про зміну стану після вивантаження.
Масиви залежностей є частиною контракту — якщо пропустити значення, з’являються застарілі функції; надмірне включення об’єктів та ефектів уповільнює кожне відображення.
Під час синхронізації з параметрами пошуку URL або зовнішніми сховищами краще використовувати спеціально створені хуки, які інкапсулюють межі дії ефектів.
Додатки, які генеруються на сервері, не повинні запускати ефекти, які працюють лише в браузері, без захисту; необхідно контролювати це через параметри середовища чи етапи життєвого циклу.
Функції очищення мають скасовувати слухачі та таймери; залишені без уваги обробники є класичною причиною попереджень про зміну стану після вивантаження додатку.
Масиви залежностей є частиною умов контракту — якщо пропустити значення, з’являються старі функції; надмірне включення об’єктів та ефектів уповільнює кожне оновлення додатку.
Під час синхронізації з параметрами пошуку URL чи зовнішніми базами даних краще використовувати спеціально створені хукі, які інкапсулюють межі дії ефектів.
Додатки, які генеруються на сервері, не повинні запускати ефекти, які працюють лише в браузері, без захисту; необхідно контролювати це через параметри середовища чи етапи життєвого циклу.
Функції очищення мають скасовувати слухачі та таймери; залишені без уваги обробники є класичною причиною попереджень про зміну стану після вивантаження додатку.
Масиви залежностей є частиною контракту — якщо пропустити значення, з’являються застарілі функції; надмірне включення об’єктів та ефектів уповільнює кожне відображення.
Під час синхронізації з параметрами пошуку URL або зовнішніми сховищами краще використовувати спеціально створені хуків, які інкапсулюють межі дії ефектів.
Додатки, які генеруються на сервері, не повинні запускати ефекти, які працюють лише в браузері, без захисних механізмів; це потрібно контролювати за умовами середовища чи життєвим циклом.
Функції очищення мають видаляти слухачів та таймери; залишені без уваги обробники є класичною причиною попереджень про зміну стану після вивантаження.
Масиви залежностей є частиною контракту — якщо пропустити значення, з’являються застарілі функції; надмірне включення об’єктів та ефектів уповільнює кожне відображення.
Під час синхронізації з параметрами пошуку URL або зовнішніми сховищами краще використовувати спеціально створені хуків, які інкапсулюють межі дії ефектів.
Додатки, які рендеруються на сервері, не повинні запускати ефекти, які працюють лише в браузері, без захисту; необхідно контролювати це через параметри середовища чи етапи життєвого циклу.
Функції очищення мають скасовувати слухачі та таймери; залишені без уваги обробники є класичною причиною попереджень про зміну стану після вивантаження додатку.
Масиви залежностей є частиною умов контракту — якщо пропустити значення, з’являються старі закриття функцій; надмірне включення об’єктів та ефектів уповільнює кожне рендерування.
Під час синхронізації з параметрами пошуку URL чи зовнішніми сховищами краще використовувати спеціально створені хуків, які інкапсулюють межі дії ефектів.
Додатки, які рендеруються на сервері, не повинні запускати ефекти, які працюють лише в браузері, без захисту; необхідно контролювати це через параметри середовища чи етапи життєвого циклу.
Функції очищення мають скасовувати слухачі та таймери; залишені без уваги обробники є класичною причиною попереджень про зміну стану після вивантаження додатку.
Масиви залежностей є частиною контракту — якщо пропустити значення, з’являються застарілі функції; якщо перелічити занадто багато об’єктів, кожна переробка сторінки стає повільною.
Під час синхронізації з параметрами пошуку URL або зовнішніми сховищами краще використовувати спеціально створені хукі, які інкапсулюють межі впливу на стан.
Додатки, які генеруються на сервері, не повинні запускати ефекти, які працюють лише в браузері, без захисних механізмів; це потрібно контролювати через параметри середовища чи життєвий цикл.
Функції очищення мають видаляти слухачі та таймери; залишені без уваги обробники є класичною причиною попереджень про зміну стану після вивантаження сторінки.