Головна / Статті

Чи заслуговує цей хук на своє місце? Десять запитань для огляду коду React

Дізнайтеся десять конкретних запитань для перевірки, які допомагають виявити зайвий стан, застарілі ефекти, цикли відрендерування, непотрібну мемоїзацію та занадто великі компоненти у коді React Hook.

Видавець: REACTAPP.TOP6 хв читання1324 слів

Джерела: Hooks

Більшість помилок у React Hook не виглядають як помилки у pull request. Відсутня залежність, ефект, який копіює параметри у стан, або використання useCallback навколо обробника, який ніхто не мемоїзує — усе це проходить перевірку під час рев’ю, а потім проявляється у вигляді застарілих екранів, зайвих відрендерувань чи накопичених слухачів. Нижче наведено десять поширених помилок з Hook у вигляді запитань для перевірки, які можна поставити до будь-якого компонента, разом із механізмом їх виникнення та простішою формою коду, яку зазвичай потрібно.

Запитання щодо стану

Чи можна обчислити це значення під час відрендерування?

Поширена практика полягає у зберіганні fullName у стані та використанні ефекту для виклику setFullName(`${firstName} ${lastName}`) щоразу, коли змінюється будь-яке із імен. Компонент відображається один раз із старим значенням, ефект виконується після оновлення інтерфейсу, а функція встановлення заплановує друге оновлення. Протягом однієї кадру інтерфейс є неузгодженим, і потрібно підтримувати додаткову рухому частину.

Краще обчислити його ось так: const fullName = `${firstName} ${lastName}`;. React перезапускає компонент під час кожного оновлення, тому значення завжди синхронізоване з його вхідними даними. Якщо щось можна отримати з пропсів або стану, це не є станом. Див. також чому синхронізація стану з useEffect має негативні наслідки.

Чи має компонент більше стану, ніж йому потрібно?

Та сама помилка у більшому масштабі: форма з трьома викликами useState для firstName, lastName та fullName. Тепер кожна зміна імені має відображатися у fullName, а будь-яка зміна, про яку не враховано, створює баг. Залиште два незалежні поля введення та обчислюйте третє. Для кожного useState перевірте, чи можна відновити значення на основі інших; якщо так, видаліть його.

Чи потрібно оновлювати екран при зміні цього значення?

Стан існує для керування процесом відображення. Ідентифікатор таймера, вузол DOM, попереднє значення властивості чи інстанція стороннього сервісу (карта, графік, клієнт-сокет) зазвичай не впливають на те, що відображається, тому useState(null) у цих випадках означає, що кожне присвоєння викликає марне оновлення відображення. Замість цього використовуйте const timerRef = useRef(null);: запис timerRef.current зберігається між оновленнями без необхідності їх запланування. Інша сторона медалі полягає у тому, що ви ніколи не повинні читати значення ref для визначення того, що відображати. Якщо інтерфейс має відображати певне значення, це все одно є станом.

Питання про ефекти

Чи перераховує масив залежностей усе, що читає ефект?

Передача [] для того, щоб ефект «виконувався один раз», як у useEffect(() => { fetchUser(userId); }, []);, заморожує значення userId після першого відображення. Коли проп змінюється, нічого не оновлюється, і інтерфейс відображає неправильного користувача. Використання [userId] вирішує цю проблему.

Масив — це не засіб керування частотою виконання; він визначає, які значення читає ефект. Якщо ефект виконується занадто часто, слід змінити те, що він читає (помістити функцію всередину, використовувати примітив замість об’єкта), а не приховувати залежності. Правило лінтингу react-hooks/exhaustive-deps виявляє майже всі такі випадки, тому треба сприймати його попередження як помилки. Ефекти, що здійснюють запити, також повинні ігнорувати або скасовувати застарілі запити під час очищення, коли userId швидко змінюється.

Чи може цей ефект спрацювати сам по собі?

useEffect(() => { setCount(count + 1); }, [count]); — це цикл: оновлення змінює count, що призводить до повторного виконання цього ефекту без кінця. Функціональний оновлювач setCount(prev => prev + 1) зчитує останнє значення без необхідності використання count у замкнутому функціоналі, тож залежність може бути видалена, і ефект виконуватиметься лише під час ініціалізації компонента. Зауважте, що він все одно збільшується на одиницю після кожної ініціалізації, а у режимі Strict Mode компонент ініціалізується двічі під час розробки, тому локально значення може збільшитися на два.

Кращим запитанням часто є те, чи взагалі оновлення має знаходитися в ефекті. Відповіді на дії користувача йдуть у обробниках подій; похідні значення обчислюються під час відображення. Ефекти призначені для синхронізації з системами за межами React.

Чи скасовує ефект те, що він налаштовує?

Ефект, який лише викликає window.addEventListener('resize', handleResize), додає нового слухача щоразу під час ініціалізації та ніколи не видаляє його. Поверніть функцію для очищення, яка викликає window.removeEventListener із тим самим обробником. React виконує її перед повторним запуском ефекту та під час його видалення. Без неї виникають витоки пам’яті, обробники виконуються кілька разів та відбуваються оновлення компонентів, яких вже немає.

Швидка перевірка: кожен add, connect або setInterval у ефекті потребує відповідного remove, disconnect або clearInterval у його значенні, яке повертається. аудит useEffect із десятьма поширеними помилками охоплює більше способів збою ефекту.

Питання щодо мемоізації

Чи справді цей обчислення є ресурсоємним?

Обгортання об’єднання рядків у useMemo із [user] як залежністю коштує більше, ніж економить. React все одно виділяє копію функції-колбека та масив, а також порівнює залежності під час кожного відображення, що є більш трудомістким, ніж просто об’єднання двох рядків. Код стає складнішим для читання, а якщо user є новим об’єктом при кожному відображенні, мемо все одно буде перераховуватися заново.

Використовуйте простий вираз. Звертайтеся до useMemo лише для дуже трудомістких операцій, таких як сортування чи фільтрація великого списку, або коли мемоїзованому дочірньому елементу чи іншому Hook потрібен стабільний посилання на об’єкт. Спочатку вимірюйте навантаження за допомогою профілера React DevTools.

Чи щось залежить від ідентичності цієї функції?

useCallback зберігає посилання на функцію стабільним, що має значення лише тоді, коли щось порівнює ці посилання: дочірній елемент, обгорнутий у React.memo, або список залежностей іншого Hook. Обгортання обробника кліку, який лише записує лог, приєднаного до звичайної button, не приносить жодної користі, оскільки DOM не цікавиться тим, чи є функція новою. Напишіть звичайну функцію та додайте useCallback, коли зможете вказати компонент, який її потребує. Компілятор React може автоматично застосовувати таку мемоізацію; перевірте поточну документацію щодо її статусу у вашій налаштуванні.

Питання щодо структури

Чи кожен Hook викликається безумовно, у тому самому порядку?

Оголошення useState лише всередині if (isLoggedIn) порушує Правила хуків. React зіставляє хуки за їхньою позицією у списку кожного компонента, а не за назвою. Якщо пропустити один виклик, кожен наступний хук буде читати дані з сусідньої позиції, що призведе до неправильних значень або помилки під час виконання через змінену кількість хуків.

Викликайте всі хуки на верхньому рівні, а потім розгалужуйтесь: спочатку оголосіть стан user, а якщо користувач не увійшов, поверніть null. Те саме стосується циклів та передчасних повернень. Щодо внутрішніх механізмів, дивіться чому існують Правила хуків.

Чи виконує цей компонент занадто багато завдань?

Компонент, який поєднує сотні рядків логіки отримання даних, стану та бізнес-логіки з маркапом, важко читати, тестувати та змінювати. Витягніть цю логіку у власний Hook. Hook useUserData(userId) керує станом user та ефектом, який викликає fetchUser(userId).then(setUser), коли змінюється userId, а потім повертає user. UserProfile скорочується до const user = useUserData(userId); та відображення user?.name.

Це дає вам:

  • Можливість повторного використання без копіювання логіки між компонентами.
  • Тести логіки, незалежні від маркапу.
  • Коротші компоненти, які легко аналізувати.
  • Одне місце для змін у отриманні даних чи кешуванні.

Пам’ятайте, що кожен користувач власного Hook отримує свій власний стан: вони діляться логікою, а не даними.

Основні висновки

  • Похідні значення — це вирази, а не стан та не ефекти.
  • Зберігайте стан у мінімальному обсязі; значення, які ніколи не потрапляють на екран, слід поміщати у refs.
  • Масиви залежностей описують, що читає ефект, тому перевіряйте їх ретельно.
  • Кожен ефект, який щось налаштовує, повинен це знешкодити.
  • Використовуйте мемоізацію лише тоді, коли це виправдано певними вимірюваннями чи потребами користувача, чутливого до посилань.
  • Викликайте Hooks безумовно та переміщуйте великі блоки логіки до користувацьких Hooks.

Більшість цих проблем виникає через звичку бездумно використовувати Hook. Запитання „Чи справді нам потрібен цей Hook тут?“ під час перевірки коштує небагато, але допомагає уникнути тривалих сеансів виправлення помилок.