← Назад к вопросам

Почему не нужно использовать useEffect без массива зависимостей?

1.2 Junior🔥 101 комментариев
#React

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

useEffect без массива зависимостей в React

Основная проблема

useEffect без массива зависимостей означает, что эффект будет запускаться после каждого render-а компонента. Это приводит к бесконечным циклам, проблемам производительности и непредсказуемому поведению.

Как это работает

// ПРОБЛЕМНЫЙ КОД — useEffect без зависимостей
function ProblematicComponent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    // Этот код запустится ПОСЛЕ КАЖДОГО RENDER-А
    fetch("/api/data")
      .then(res => res.json())
      .then(result => setData(result));
    // setData триггерит re-render
    // re-render триггерит useEffect снова
    // = БЕСКОНЕЧНЫЙ ЦИКЛ!
  }); // <- ни массива зависимостей!
  
  return <div>{data?.title}</div>;
}

// Цикл:
// 1. Component render
// 2. useEffect запускается
// 3. setData(result) триггерит re-render
// 4. useEffect запускается ещё раз
// 5. Шаг 3-4 повторяются бесконечно
// 6. API получает тысячи запросов!

Почему это плохо

1. Бесконечные циклы

function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // Запускается после каждого render-а
    setCount(count + 1); // Вызывает re-render
  }); // Без зависимостей!
  
  return <div>{count}</div>; // Render снова → useEffect снова → ...
}

// Результат:
// count: 0 → 1 → 2 → 3 → 4 → ... → 999 → максимум call stack
// Страница падает с ошибкой!

2. Нежелательные API запросы

function ProductList() {
  const [products, setProducts] = useState([]);
  
  useEffect(() => {
    // Запускается после КАЖДОГО render-а
    fetch("/api/products").then(res => res.json());
  }); // Без зависимостей
  
  // Результат:
  // 1. Initial render → API call 1
  // 2. Re-render → API call 2
  // 3. Re-render → API call 3
  // ... за 1 секунду: 100+ запросов!
  // Сервер думает что это DDoS
}

4. Утечки памяти (memory leaks)

function ChatComponent() {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    // Запускается после каждого render-а
    // Но cleanup не вызывается перед следующим effet-ом!
    const socket = io("ws://chat");
    socket.on("message", msg => setMessages(prev => [...prev, msg]));
    
    // НЕ ВЫЗЫВАЕТСЯ cleanup!
    // return () => socket.disconnect();
  }); // Без зависимостей
  
  // Результат:
  // 1. Component render → socket создан
  // 2. Сообщение приходит → setMessages → re-render
  // 3. useEffect запускается снова → НОВЫЙ socket создан
  // 4. Теперь 2 socket-а слушают одновременно
  // 5. Каждая итерация добавляет новый socket
  // = утечка памяти, каждое сообщение дублируется
}

5. Непредсказуемое поведение

function FormComponent() {
  const [formData, setFormData] = useState({});
  const [errors, setErrors] = useState({});
  
  useEffect(() => {
    // Вызывается при КАЖДОМ render-е
    // Может трудно проследить когда что происходит
    console.log("Валидирую форму", formData);
    
    if (!formData.email) setErrors({email: "Required"});
    // Или запускается сложный расчёт
    // Или отправляется запрос
  }); // Без зависимостей
  
  // Проблема: что повторяется?
  // - Когда юзер печатает в input?
  // - Всегда ли валидация?
  // - Когда отправляется запрос?
  // Непонятно!
}

Правильное использование useEffect

1. С пустым массивом — только при монтировании

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    // Запускается ТОЛЬКО при монтировании компонента
    setLoading(true);
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data))
      .finally(() => setLoading(false));
  }, []); // ПУСТОЙ массив зависимостей!
  
  if (loading) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

2. С конкретными зависимостями

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    // Запускается когда query меняется
    if (!query) {
      setResults([]);
      return;
    }
    
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => setResults(data));
  }, [query]); // Зависим от query!
  
  return (
    <ul>
      {results.map(r => <li key={r.id}>{r.title}</li>)}
    </ul>
  );
}

3. С cleanup функцией

function ChatWindow() {
  const [messages, setMessages] = useState([]);
  
  useEffect(() => {
    const socket = io("ws://chat");
    
    socket.on("message", msg => {
      setMessages(prev => [...prev, msg]);
    });
    
    // CLEANUP функция — вызывается перед umount или перед следующим effect
    return () => {
      socket.disconnect();
    };
  }, []); // Пустой массив — эффект один раз, cleanup при umount
  
  return <div>{messages.map(m => <p key={m.id}>{m.text}</p>)}</div>;
}

4. С множественными зависимостями

function FilteredList({ category, sortBy }) {
  const [items, setItems] = useState([]);
  
  useEffect(() => {
    // Запускается когда category ИЛИ sortBy меняются
    fetch(`/api/items?category=${category}&sort=${sortBy}`)
      .then(res => res.json())
      .then(data => setItems(data));
  }, [category, sortBy]); // Оба параметра — зависимости
  
  return <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>;
}

Таблица различных вариантов

// 1. БЕЗ МАССИВА ЗАВИСИМОСТЕЙ (ПЛОХО!)
useEffect(() => {
  // Запускается после КАЖДОГО render-а
  // Может привести к циклам и утечкам памяти
});

// 2. С ПУСТЫМ МАССИВОМ (ХОРОШО для инициализации)
useEffect(() => {
  // Запускается только при монтировании
  // Cleanup (если есть) вызывается при umount
}, []);

// 3. С ЗАВИСИМОСТЯМИ (ХОРОШО для реактивности)
useEffect(() => {
  // Запускается при изменении зависимостей
  // Cleanup вызывается перед следующим effect-ом
}, [dep1, dep2]);

// 4. С АБСОЛЮТНО ВСЕМИ ЗАВИСИМОСТЯМИ (ПРАВИЛЬНО)
const memoizedCallback = useCallback(() => {
  // ...
}, [dep1, dep2]);

useEffect(() => {
  // Вызывает callback только если deps изменились
  memoizedCallback();
}, [memoizedCallback]); // Зависим от самого callback!

Частые ошибки

Ошибка 1: Забыли зависимость

// НЕПРАВИЛЬНО
function Component({ userId }) {
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data));
  }, []); // ЗАБЫЛИ userId в зависимостях!
  
  // Результат:
  // userId меняется (3 → 4)
  // useEffect НЕ запускается снова
  // Показываем данные старого userId!
}

// ПРАВИЛЬНО
useEffect(() => {
  fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => setUser(data));
}, [userId]); // ✓ Включили зависимость

Ошибка 2: Функция как зависимость без useMemo

// НЕПРАВИЛЬНО — бесконечный цикл
function Component() {
  const [data, setData] = useState(null);
  
  const fetchData = () => {
    return fetch("/api/data").then(r => r.json());
  }; // Новая функция при каждом render-е!
  
  useEffect(() => {
    fetchData();
  }, [fetchData]); // fetchData всегда "новая" → бесконечный loop
}

// ПРАВИЛЬНО — используем useCallback
function Component() {
  const [data, setData] = useState(null);
  
  const fetchData = useCallback(() => {
    return fetch("/api/data").then(r => r.json());
  }, []); // Функция стабильна
  
  useEffect(() => {
    fetchData();
  }, [fetchData]); // OK, нет цикла
}

React DevTools подсказка

Если у вас ESLint с eslint-plugin-react-hooks:

// Будет предупреждение:
// "React Hook useEffect has a missing dependency: userId"
// СЛУШАЙТЕ ЭТО ПРЕДУПРЕЖДЕНИЕ!

// Можете отключить с комментарием, но нужна хорошая причина
useEffect(() => {
  // ...
}, []); // eslint-disable-next-line react-hooks/exhaustive-deps
// ПЛОХО! Не отключайте без причины!

Практический пример — неправильно и правильно

// НЕПРАВИЛЬНО
function UserSearch() {
  const [searchTerm, setSearchTerm] = useState("");
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    // Запускается при КАЖДОМ render-е!
    if (searchTerm) {
      fetch(`/api/search?q=${searchTerm}`)
        .then(r => r.json())
        .then(setResults);
    }
  }); // ← БЕЗ МАССИВА!
}

// ПРАВИЛЬНО
function UserSearch() {
  const [searchTerm, setSearchTerm] = useState("");
  const [results, setResults] = useState([]);
  
  useEffect(() => {
    // Запускается только когда searchTerm меняется
    if (searchTerm) {
      fetch(`/api/search?q=${searchTerm}`)
        .then(r => r.json())
        .then(setResults);
    }
  }, [searchTerm]); // ← С МАССИВОМ ЗАВИСИМОСТЕЙ
}

Вывод

useEffect без массива зависимостей — это почти всегда ошибка, которая приводит к:

  1. Бесконечным циклам — падение приложения
  2. Лишним запросам — нагрузка на сервер
  3. Утечкам памяти — браузер замедляется
  4. Непредсказуемому поведению — сложно дебажить

Правильный подход:

  • [] — эффект при монтировании
  • [deps] — эффект когда зависимости меняются
  • Всегда включайте все переменные из внешнего scope в массив зависимостей
  • Используйте ESLint react-hooks для проверки
  • Слушайте предупреждения компилятора!