Как я перестал злоупотреблять useEffect и начал жить

#react#hooks#performance

TL;DR: useEffect — это не серебряная пуля для управления побочными эффектами. Чрезмерное использование этого хука приводит к хрупким компонентам, race condition и проблемам с производительностью. Современные React-паттерны предлагают более элегантные решения через композицию, серверные компоненты и правильное управление состоянием.

Эффектозависимость как антипаттерн

Проблема, описанная в Reddit-посте — классический пример “эффектозависимости” (effect addiction), когда разработчики используют useEffect как универсальный инструмент для реагирования на изменения в приложении. Это приводит к каскаду эффектов, где один хук триггерит другой, создавая хрупкие цепочки зависимостей.

Рассмотрим типичный антипаттерн:

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [posts, setPosts] = useState([]);
  
  // Эффект для загрузки пользователя
  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);
  
  // Эффект для загрузки постов после получения пользователя
  useEffect(() => {
    if (user) {
      fetchPosts(user.id).then(setPosts);
    }
  }, [user]);
  
  // ...рендер логика
}

Такая реализация создает несколько проблем:

  1. Ненужные последовательные запросы (waterfall)
  2. Возможные race condition при быстрой смене userId
  3. Сложность тестирования из-за множества побочных эффектов

Современные альтернативы useEffect

1. Вынесение логики в обработчики событий

Многие “эффекты” на самом деле должны быть обработчиками конкретных действий пользователя:

function SearchForm() {
  const [query, setQuery] = useState('');
  
  // ❌ Плохо - эффект на каждое изменение
  // useEffect(() => {
  //   fetchResults(query);
  // }, [query]);
  
  // ✅ Хорошо - явный обработчик
  const handleSubmit = (e) => {
    e.preventDefault();
    fetchResults(query);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
    </form>
  );
}

2. Использование композиции и производных состояний

React 18 активно продвигает концепцию “просто рендерить” (just render). Вместо эффектов для синхронизации состояний можно вычислять значения прямо во время рендера:

function CartSummary({ items }) {
  // ❌ Избыточное состояние
  // const [total, setTotal] = useState(0);
  // useEffect(() => {
  //   setTotal(items.reduce((sum, item) => sum + item.price, 0));
  // }, [items]);
  
  // ✅ Производное значение
  const total = items.reduce((sum, item) => sum + item.price, 0);
  
  return <div>Total: {total}</div>;
}

3. Использование серверных компонентов (RSC)

С появлением React Server Components часть логики можно перенести на сервер, полностью исключив необходимость в эффектах для загрузки данных:

// app/user/page.js
async function UserPage({ params }) {
  const user = await fetchUser(params.id);
  const posts = await fetchPosts(user.id);
  
  return (
    <>
      <UserProfile user={user} />
      <PostsList posts={posts} />
    </>
  );
}

Когда useEffect действительно нужен

Несмотря на злоупотребления, у useEffect есть законные случаи применения:

  1. Интеграция с внешними библиотеками (например, инициализация карты)
  2. Подписки на события (WebSocket, window.addEventListener)
  3. Анимации и измерения DOM
function useAnimation(selector) {
  useEffect(() => {
    const element = document.querySelector(selector);
    if (!element) return;
    
    const animation = element.animate(...);
    return () => animation.cancel();
  }, [selector]);
}

Практические шаги по рефакторингу

  1. Анализ существующих эффектов:

    • Какие из них реагируют на пользовательские действия?
    • Какие синхронизируют состояния?
    • Какие работают с внешними системами?
  2. Применение паттернов:

    • Замена цепочек эффектов на async/await в обработчиках
    • Вынесение сложной логики в кастомные хуки
    • Использование React Query или SWR для данных
  3. Пример рефакторинга:

До:

function ProductPage({ id }) {
  const [product, setProduct] = useState(null);
  const [reviews, setReviews] = useState([]);
  
  useEffect(() => {
    fetchProduct(id).then(setProduct);
  }, [id]);
  
  useEffect(() => {
    if (product) {
      fetchReviews(product.slug).then(setReviews);
    }
  }, [product]);
  
  // ...
}

После:

function ProductPage({ id }) {
  const { data: product } = useQuery(['product', id], () => fetchProduct(id));
  const { data: reviews } = useQuery(
    ['reviews', product?.slug],
    () => fetchReviews(product.slug),
    { enabled: !!product }
  );
  
  // ...
}

Заключение

Избавление от избыточных эффектов приводит к более предсказуемым и производительным компонентам. Современный React предлагает множество инструментов (композиция, серверные компоненты, библиотеки управления состоянием), которые делают useEffect специализированным инструментом, а не универсальным решением.

Как показывает практика, после рефакторинга 80% эффектов оказываются ненужными, а оставшиеся 20% становятся более осмысленными и управляемыми. Главное — помнить: если кажется, что без useEffect не обойтись, стоит проверить документацию React — возможно, уже есть более элегантное решение.


Источник: https://www.reddit.com/r/reactjs/comments/1rwwsbz/finally_realized_how_much_i_was_abusing_useeffect/