Перестаньте использовать 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 на основе параметров/состояния, часто содержат запрещенные циклы обратной связи.
2. Дорогостоящие вычисления в эффектах
Чистые вычисления следует выполнять в процессе отрисовки/с использованием useMemo, а не после её завершения.
3. Загрузка данных как неконтролируемый эффект
Загрузка данных требует возможности отмены, кэширования и удобного отображения ошибок. Лучше использовать специализированные библиотеки или фреймворки с механизмами отображения состояния загрузки; если используются эффекты, необходимо обрабатывать ситуации отмены и конфликты.
4. Действия пользователя в эффектах
Обработчики кликов должны находиться внутри событий. Эффекты, запускаемые при изменении флага в результате клика, приводят к косвенным операциям и ошибкам.
Дерево принятия решений
Если вычисления основаны на существующем состоянии/параметрах → отрисовка. Если операция реагирует на клик/отправку формы → обработчик события. Если операция взаимодействует с API DOM, сетевым источником или жизненным циклом стороннего компонента → эффект.
Когда подходят эффекты
Внешние обработчики событий
WebSockets
Инициализация библиотек сторонних разработчиков
Императивные API, не связанные с React, которые должны корректно очищаться
Меньше эффектов, более понятный поток данных. Используйте useEffect только для синхронизации с системами вне React, а не как универсальный инструмент для каждой задачи после отрисовки.
Режим строгой проверки дважды вызывает эффекты во время разработки, чтобы выявить недостающие операции очистки — рассматривайте это как преимущество при работе с соединениями типа WebSocket. Для управления состоянием сервера предпочитайте декларативные библиотеки данных, чтобы компоненты описывали свои зависимости, а не создавали собственные функции загрузки данных. Оцените ситуацию перед добавлением эффектов, которые просто копируют свойства в состояние; такой подход обычно является недопониманием процесса отрисовки.
Функции очистки должны удалять обработчики событий и таймеры; оставшиеся обработчики являются классической причиной предупреждений о вызове setState после снятия компонента с экрана.
Массивы зависимостей являются частью контракта — если пропустить значения, появятся устаревшие замыкания; чрезмерное указание объектов и эффектов приводит к замедлению каждой отрисовки.
При синхронизации с параметрами поиска URL или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.
Приложения, генерируемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или жизненного цикла.
Функции очистки должны удалять обработчики событий и таймеры; оставшиеся обработчики являются классической причиной предупреждений о изменении состояния после демонтирования компонента.
Массивы зависимостей являются частью контракта — если пропустить значения, появятся устаревшие замыкания; чрезмерное указание объектов и эффектов приводит к замедлению каждой отрисовки.
При синхронизации с параметрами поиска URL или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.
Приложения, генерируемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или этапа жизненного цикла.
Функции очистки обязаны удалять слушатели и таймеры; оставшиеся обработчики являются распространенной причиной предупреждений о изменении состояния после демонтирования компонента.
Массивы зависимостей являются частью спецификации — их пропуск приводит к появлению устаревших закрытий функций; чрезмерное указание объектов и эффектов замедляет каждую перерисовку.
При синхронизации с параметрами поиска URL или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.
Приложения, генерируемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или этапа жизненного цикла.
Функции очистки обязаны удалять слушатели и таймеры; оставшиеся обработчики являются распространенной причиной предупреждений о изменении состояния после демонтирования компонента.
Массивы зависимостей являются частью контракта — если пропустить значения, появятся устаревшие замыкания; чрезмерное указание объектов и эффектов приводит к замедлению каждой отрисовки.
При синхронизации с параметрами поиска URL или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.
Приложения, отрисовываемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или жизненного цикла.
Функции очистки должны удалять обработчики событий и таймеры; оставшиеся обработчики являются классической причиной предупреждений о изменении состояния после демонтирования компонента.
Массивы зависимостей являются частью контракта — если пропустить значения, появятся устаревшие замыкания; чрезмерное указание объектов и эффектов приводит к замедлению каждой отрисовки.
При синхронизации с параметрами поиска URL или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.
Приложения, генерируемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или этапа жизненного цикла.
Функции очистки обязаны удалять слушатели и таймеры; оставшиеся обработчики являются распространенной причиной предупреждений о изменении состояния после демонтирования компонента.
Массивы зависимостей являются частью спецификации — их пропуск приводит к появлению устаревших закрытий функций; чрезмерное указание объектов и эффектов замедляет каждую перерисовку.
При синхронизации с параметрами поиска URL или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.
Приложения, генерируемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или этапа жизненного цикла.
Функции очистки обязаны удалять слушатели и таймеры; оставшиеся обработчики являются распространенной причиной предупреждений о изменении состояния после демонтирования компонента.
Массивы зависимостей являются частью контракта — при их опущении появляются устаревшие закрытия; чрезмерное перечисление объектов приводит к замедлению каждой отрисовки.
При синхронизации с параметрами поиска URL или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.
Приложения, отрисовываемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или жизненного цикла.
Функции очистки должны удалять обработчики событий и таймеры; оставшиеся обработчики являются классической причиной предупреждений о изменении состояния после демонтирования компонента.