Главная / Статьи / Перестаньте использовать useEffect для решения каждой проблемы в React.

Перестаньте использовать useEffect для решения каждой проблемы в React.

Эффекты синхронизируются с внешними системами; генерируются в процессе рендеринга, обрабатываются при нажатии кнопок в событиях, загружаются с помощью инструментов для работы с реальными данными.

1424 слов

Ловушка 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 или внешними хранилищами предпочтительно использовать специально созданные хуки, которые обеспечивают изоляцию эффектов.

Приложения, отрисовываемые на сервере, не должны запускать эффекты, доступные только в браузере, без защитных механизмов; необходимо контролировать это в зависимости от среды или жизненного цикла.

Функции очистки должны удалять обработчики событий и таймеры; оставшиеся обработчики являются классической причиной предупреждений о изменении состояния после демонтирования компонента.