Какие существуют правила использования хуков в React?

👨‍💻 Frontend Developer 🟡 Часто попадается 🎚️ Средний
#React #Hooks

Краткий ответ

В React существует два основных правила использования хуков:

Правило 1: Вызывайте хуки только на верхнем уровне

  • Не вызывайте хуки внутри циклов, условий или вложенных функций
  • Хуки должны вызываться в том же порядке при каждом рендере

Правило 2: Вызывайте хуки только в функциональных компонентах React или пользовательских хуках

  • Не вызывайте хуки в обычных JavaScript-функциях
  • Не вызывайте хуки в классовых компонентах

Ключевое правило: Соблюдайте порядок вызова хуков и не вызывайте их условно! 🎯


Полный ответ

Представьте, что хуки — это как ингредиенты в рецепте. Если вы каждый раз добавляете их в разном порядке или пропускаете некоторые, блюдо получится непредсказуемым! 🍳

Основные правила хуков

React полагается на порядок вызова хуков для правильной работы:

// ✅ Правильно — хуки на верхнем уровне
function MyComponent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('');
  
  useEffect(() => {
    // Эффект
  }, []);
  
  return (
    <div>
      <p>{count} - {name}</p>
      <button onClick={() => setCount(count + 1)}>
        Увеличить
      </button>
    </div>
  );
}
 
// ❌ Неправильно&nbsp;— хуки в&nbsp;условиях
function BadComponent({ condition }) {
  if (condition) {
    const [state, setState] = useState(0); // ОШИБКА!
  }
  
  return <div>Компонент</div>;
}

Почему важен порядок хуков

React использует внутренний список для отслеживания хуков:

// Как React видит хуки:
// Первый рендер:
// 1. useState(0) -> [0, setter]
// 2. useState('') -> ['', setter]
// 3. useEffect(() => {...}) -> undefined
 
// Второй рендер:
// 1. useState(0) -> [0, setter] // Тот же хук!
// 2. useState('') -> ['', setter] // Тот же хук!
// 3. useEffect(() => {...}) -> undefined // Тот же хук!

Распространенные ошибки

1. Условные вызовы хуков

// ❌ Неправильно - условный вызов
function BadComponent({ isLoggedIn }) {
  const [user, setUser] = useState(null);
  
  if (isLoggedIn) {
    // Это нарушает порядок хуков!
    const [token, setToken] = useState('');
  }
  
  return <div>Компонент</div>;
}
 
// ✅ Правильно - всегда вызывайте хуки
function GoodComponent({ isLoggedIn }) {
  const [user, setUser] = useState(null);
  const [token, setToken] = useState(''); // Всегда вызывается
  
  // Используйте условную логику внутри хуков
  useEffect(() => {
    if (isLoggedIn) {
      // Логика для авторизованных пользователей
    }
  }, [isLoggedIn]);
  
  return <div>Компонент</div>;
}

2. Циклы и хуки

// ❌ Неправильно - хуки в циклах
function BadComponent({ items }) {
  const [state, setState] = useState(null);
  
  items.forEach(item => {
    // Каждый рендер может иметь разное количество элементов!
    const [itemState, setItemState] = useState(item); // ОШИБКА!
  });
  
  return <div>Компонент</div>;
}
 
// ✅ Правильно - хуки вне циклов
function GoodComponent({ items }) {
  const [state, setState] = useState(null);
  
  // Используйте массив состояний
  const [itemStates, setItemStates] = useState(
    items.map(item => item)
  );
  
  return (
    <div>
      {items.map((item, index) => (
        <div key={index}>
          <input 
            value={itemStates[index]}
            onChange={e => {
              const newStates = [...itemStates];
              newStates[index] = e.target.value;
              setItemStates(newStates);
            }}
          />
        </div>
      ))}
    </div>
  );
}

3. Вызов хуков в обычных функциях

// ❌ Неправильно - хуки в обычных функциях
function regularFunction() {
  const [state, setState] = useState(0); // ОШИБКА!
  return state;
}
 
// ✅ Правильно - хуки в компонентах или пользовательских хуках
function Component() {
  const value = useCustomHook(); // Правильно
  return <div>{value}</div>;
}
 
function useCustomHook() {
  const [state, setState] = useState(0); // Правильно
  return state;
}

Правила для пользовательских хуков

Именование

// ✅ Пользовательские хуки начинаются с "use"
function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);
  
  const increment = () => setCount(count + 1);
  const decrement = () => setCount(count - 1);
  const reset = () => setCount(initialValue);
  
  return { count, increment, decrement, reset };
}
 
// ❌ Неправильное именование
function counterHook() { // Должно начинаться с "use"
  const [count, setCount] = useState(0);
  return [count, setCount];
}

Правильное использование пользовательских хуков

// ✅ Правильное использование
function UserProfile() {
  const { count, increment, decrement, reset } = useCounter(0);
  
  return (
    <div>
      <p>Счетчик: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
      <button onClick={reset}>Сброс</button>
    </div>
  );
}
 
// ✅ Пользовательский хук для API
function useApi(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(url);
        const result = await response.json();
        setData(result);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [url]);
  
  return { data, loading, error };
}

Лучшие практики

1. Группировка связанных состояний

// ❌ Слишком много отдельных состояний
function BadForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  // ... еще состояния
  
  return (
    <form>
      {/* Поля формы */}
    </form>
  );
}
 
// ✅ Группировка в объект
function GoodForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: '',
    confirmPassword: ''
  });
  
  const updateField = (field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: value
    }));
  };
  
  return (
    <form>
      <input 
        value={formData.name}
        onChange={e => updateField('name', e.target.value)}
      />
      <input 
        value={formData.email}
        onChange={e => updateField('email', e.target.value)}
      />
      {/* Остальные поля */}
    </form>
  );
}

2. Оптимизация useEffect

// ❌ Неоптимальный useEffect
function BadComponent({ userId, postId }) {
  const [user, setUser] = useState(null);
  const [post, setPost] = useState(null);
  
  // Выполняется при изменении любого пропса
  useEffect(() => {
    fetchUser(userId).then(setUser);
    fetchPost(postId).then(setPost);
  }, [userId, postId]); // Оба пропса в зависимостях
  
  return <div>Компонент</div>;
}
 
// ✅ Разделенные эффекты
function GoodComponent({ userId, postId }) {
  const [user, setUser] = useState(null);
  const [post, setPost] = useState(null);
  
  // Отдельные эффекты для разных данных
  useEffect(() => {
    if (userId) {
      fetchUser(userId).then(setUser);
    }
  }, [userId]); // Только userId
  
  useEffect(() => {
    if (postId) {
      fetchPost(postId).then(setPost);
    }
  }, [postId]); // Только postId
  
  return <div>Компонент</div>;
}

Когда использовать хуки

1. useState для локального состояния

// ✅ Простое состояние
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Счетчик: {count}
    </button>
  );
}

2. useEffect для побочных эффектов

// ✅ Подписки и очистка
function WindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight
  });
  
  useEffect(() => {
    const handleResize = () => {
      setSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };
    
    window.addEventListener('resize', handleResize);
    
    // Очистка подписки
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  
  return (
    <div>
      Размер окна: {size.width} x {size.height}
    </div>
  );
}

3. Пользовательские хуки для повторного использования

// ✅ Переиспользуемый хук
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });
  
  const setValue = (value) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
}
 
// Использование
function Settings() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [language, setLanguage] = useLocalStorage('language', 'ru');
  
  return (
    <div>
      <select value={theme} onChange={e => setTheme(e.target.value)}>
        <option value="light">Светлая</option>
        <option value="dark">Темная</option>
      </select>
      
      <select value={language} onChange={e => setLanguage(e.target.value)}>
        <option value="ru">Русский</option>
        <option value="en">English</option>
      </select>
    </div>
  );
}

Резюме

Правила хуков — это как правила дорожного движения для React! 🚦

  • Правило 1: Вызывайте хуки только на верхнем уровне ✅
  • Правило 2: Вызывайте хуки только в функциональных компонентах или пользовательских хуках ✅

Когда нарушать нельзя:

  • Никогда не вызывайте хуки условно ❌
  • Никогда не вызывайте хуки в циклах ❌
  • Никогда не вызывайте хуки в обычных функциях ❌

Практические советы:

  1. Используйте ESLint-плагин для React Hooks
  2. Называйте пользовательские хуки с префикса “use”
  3. Группируйте связанные состояния
  4. Разделяйте сложные эффекты на несколько useEffect

Соблюдение правил хуков — залог стабильной и предсказуемой работы ваших React-приложений! 💪


Хотите больше полезных статей о React? Подписывайтесь на EasyAdvice, добавляйте сайт в закладки и прокачивайтесь каждый день! 🚀