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

Как работают зависимости в React?

2.0 Middle🔥 131 комментариев
#React#Архитектура и паттерны

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

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

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

Как работают зависимости в React

Dependency array (массив зависимостей) - это критическая часть React хуков. Это механизм, который сообщает React, когда нужно пересчитать результаты хуков. Неправильное использование зависимостей - частая причина багов и утечек памяти.

Базовая концепция

Массив зависимостей используется в хуках: useState, useEffect, useMemo, useCallback, useReducer.

import { useEffect, useMemo, useCallback } from 'react';

function Component({ userId }) {
  // Пересчитается ТОЛЬКО когда userId изменится
  useEffect(() => {
    console.log('userId changed to:', userId);
  }, [userId]);

  // Пересчитается когда userId или count изменится
  useMemo(() => {
    return userId + count;
  }, [userId, count]);

  // Создаст новую функцию только когда userId изменится
  useCallback(() => {
    api.updateUser(userId);
  }, [userId]);
}

Как React отслеживает зависимости

React использует Object.is для сравнения значений между рендерами:

// Object.is - строгое сравнение
console.log(Object.is(3, 3)); // true
console.log(Object.is('hello', 'hello')); // true
console.log(Object.is({}, {})); // false (разные объекты!)
console.log(Object.is(NaN, NaN)); // true (отличие от ===)

// React сравнивает каждый элемент массива
const deps1 = [userId, count];
const deps2 = [userId, count];
// React считает их РАЗНЫМИ, т.к. это разные массивы!

const deps3 = deps1;
// А это тот же массив - React считает их ОДИНАКОВЫМИ

Правила использования зависимостей

1. Правильно указывать ВСЕ зависимости

// ПЛОХО - забыли userId в зависимостях
function Component({ userId }) {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('userId:', userId); // Всегда будет старое значение!
    }, 1000);
    return () => clearInterval(timer);
  }, []); // Пустой массив = эффект выполнится один раз
}

// ПРАВИЛЬНО
function Component({ userId }) {
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('userId:', userId);
    }, 1000);
    return () => clearInterval(timer);
  }, [userId]); // userId добавлена
}

2. Пустой массив = выполнить один раз

function Component() {
  // Выполнится один раз при монтировании
  useEffect(() => {
    console.log('Component mounted');
    return () => console.log('Component unmounted');
  }, []);

  return <div>Component</div>;
}

3. Без массива = выполнить КАЖДЫЙ рендер

function Component() {
  // ОПАСНО! Выполнится при каждом рендере
  useEffect(() => {
    console.log('This runs on EVERY render');
  }); // Нет массива зависимостей!

  return <div>Component</div>;
}

Практические примеры

1. Загрузка данных

function UserProfile({ userId }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Загрузится при изменении userId
  useEffect(() => {
    let isMounted = true;

    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        
        // Проверяем, еще ли компонент смонтирован
        if (isMounted) {
          setUser(data);
          setError(null);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // Очистка при размонтировании или изменении userId
    return () => {
      isMounted = false;
    };
  }, [userId]); // ВАЖНО: userId включен

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  return <div>{user?.name}</div>;
}

2. Дебоунс поиска

function SearchUsers() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<User[]>([]);

  // Дебоунс с useEffect
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }

    // Задержка выполнения
    const timer = setTimeout(async () => {
      const data = await fetch(`/api/search?q=${query}`)
        .then(r => r.json());
      setResults(data);
    }, 500);

    // Очистка предыдущего timer при изменении query
    return () => clearTimeout(timer);
  }, [query]); // Пересчитается при изменении query

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <ul>
        {results.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </>
  );
}

3. Подписка на событие

function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (e: MouseEvent) => {
      setPosition({ x: e.clientX, y: e.clientY });
    };

    // Подписаться на событие
    window.addEventListener('mousemove', handleMouseMove);

    // Очистить подписку
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // Только один раз при монтировании

  return <div>X: {position.x}, Y: {position.y}</div>;
}

Проблемы с зависимостями

Проблема 1: Бесконечный цикл

function Component() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState(null);

  // ПЛОХО - бесконечный цикл!
  useEffect(() => {
    const config = { count }; // Новый объект каждый раз
    // ...
  }, [{ count }]); // Новый объект в зависимостях!

  // ПРАВИЛЬНО
  useEffect(() => {
    // ...
  }, [count]); // Примитивное значение
}

Проблема 2: Утечка памяти

// ПЛОХО - подписка никогда не удаляется
function Observer() {
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      console.log('Store changed');
    });
    // Забыли вернуть cleanup!
  }, []);
}

// ПРАВИЛЬНО
function Observer() {
  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      console.log('Store changed');
    });
    return () => unsubscribe(); // Очистка
  }, []);
}

Проблема 3: Объекты и массивы

function Parent() {
  // ПЛОХО - новый объект каждый рендер
  const config = { timeout: 1000 };
  return <Child config={config} />;
}

function Child({ config }) {
  useEffect(() => {
    console.log('Effect runs every render!');
  }, [config]); // config всегда новый объект
}

// ПРАВИЛЬНО
function Parent() {
  const config = useMemo(() => ({ timeout: 1000 }), []);
  return <Child config={config} />;
}

function Child({ config }) {
  useEffect(() => {
    console.log('Effect runs only on mount');
  }, [config]); // config один и тот же
}

useCallback vs useMemo

function Component({ userId }) {
  // useMemo - мемоизирует результат вычисления
  const expensiveValue = useMemo(() => {
    return calculateComplexValue(userId);
  }, [userId]);

  // useCallback - мемоизирует функцию
  const handleClick = useCallback(() => {
    api.updateUser(userId);
  }, [userId]);

  return (
    <>
      <div>{expensiveValue}</div>
      <button onClick={handleClick}>Update</button>
    </>
  );
}

ESLint правила

Используй плагин eslint-plugin-react-hooks:

{
  "plugins": ["react-hooks"],
  "rules": {
    "react-hooks/rules-of-hooks": "error",
    "react-hooks/exhaustive-deps": "warn"
  }
}

Это будет предупреждать о забытых зависимостях:

// ESLint предупредит: 'userId' is missing from dependencies
useEffect(() => {
  fetchUser(userId);
}, []); // userId забыт!

Checklist для правильной работы с зависимостями

  1. Включи ВСЕ переменные, используемые в эффекте
  2. Не включай функции, если они не меняются (обверни в useCallback)
  3. Не создавай объекты внутри компонента для зависимостей
  4. Используй пустой массив только для эффектов при монтировании
  5. Всегда добавляй cleanup функцию для подписок
  6. Используй ESLint плагин для автоматической проверки
Как работают зависимости в React? | PrepBro