Головна / Статті / Чек-лист для перевірки коду на React: похідний стан, редюсери, ефекти та Memo

Чек-лист для перевірки коду на React: похідний стан, редюсери, ефекти та Memo

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

1906 слів

Розробники з кількома роками досвіду роботи з React зазвичай знають, як передаються пропси, як функціонують хуки та чому стан ніколи не слід змінювати безпосередньо. Проте під час перевірки коду у їхніх запитах на інтеграцію постійно з’являються одні й ті самі проблеми: компоненти, які працюють, але є крихкими, важкими для тестування, надмірно складними або важкими для розширення. Щоб подолати цю стадію, не так вже й важливо вивчати незрозумілі API, як скоригувати уявлення про те, де знаходиться стан, для чого потрібні певні ефекти та коли оптимізація приносить користь. Цей чек-лист розглядає сім поширених проблем, використовуючи список завдань у TypeScript як приклад, та показує підхід, який слід застосовувати в кожному випадку.

1. Стан, який слід отримувати шляхом обчислень

Основне правило стану в React — зберігати єдине джерело істини. Коли однакова інформація знаходиться у двох місцях, копії зрештою будуть неузгоджені.

Як це виглядає

Список завдань, який має враховувати як усі елементи, так і завершені, часто призводить до наявності двох масивів у стані:

// ❌ BAD: Storing derived state in two separate places
const [todos, setTodos] = useState<Todo[]>([]);
const [completedTodos, setCompletedTodos] = useState<Todo[]>([]);

Тепер кожну зміну доводиться застосовувати двічі. Якщо користувач змінює назву завдання, а код оновлює todos, але забуває про completedTodos, інтерфейс відображає стару назву в одному списку та нову — у іншому.

Що писати замість цього

Зберігайте один масив у стані та обчислюйте завершену підмножину під час відображення:

// ✅ GOOD: Single source of truth with derived state
const [todos, setTodos] = useState<Todo[]>([]);
// Derived value recalculated on every render (inexpensive computation)
const completedTodos = todos.filter((todo) => todo.completed);

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

2. Ручне отримання даних у useEffect

Ручні виклики fetch усередині useEffect виглядають простими, але вони схильні до виникнення ситуацій конкуренції, не мають функції кешування та вимагають ручного керування станом завантаження та помилок у кожному компоненті.

Як це виглядає

Цей ефект завантажує список один раз та використовує флаг isMounted, щоб не змінювати стан після видалення компонента:

// ❌ BAD: Boilerplate-heavy, error-prone, no caching or deduplication
useEffect(() => {
  let isMounted = true;
  fetch('/api/todos')
    .then((res) => res.json())
    .then((data) => {
      if (isMounted) setTodos(data);
    });
  return () => { isMounted = false; };
}, []);

У ньому все одно є пробели. Немає обробки помилок, немає стану завантаження, немає усунення дублікатів, коли два компоненти запитують однакові дані, немає кешування між екранами та немає можливості повторної спроби. Флаг isMounted також лише пригнічує запізнюване оновлення; він не скасовує запит, як це робить AbortController.

Що писати замість цього

Використовуйте бібліотеку, створену для керування станом сервера, таку як TanStack Query, SWR або RTK Query. Відповідний виклик TanStack Query оголошує ключ кешу та функцію отримання даних, а потім повертає дані, інформацію про стан завантаження та повідомлення про помилки:

// ✅ GOOD: Declarative, automatically cached, auto-retried
const { data: todos, isLoading, error } = useQuery({
  queryKey: ['todos'],
  queryFn: fetchTodos,
});

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

3. Купа викликів useState, де може поміститися редюсер

useState ідеально підходить для простих значень, таких як перемикач чи поле введення тексту. Однак коли оновлення залежать від попередніх значень або стан включає вкладені масиви та об’єкти, які змінюються кількома різними діями, використання низки функцій-встановлювачів робить компоненти надмірно об’ємними та сприяє виникненню помилок через застарілий стан.

Як це виглядає

Один компонент оголошує від п’яти до десяти гаків useState та оновлює їх через багато функцій-викликів для додавання, редагування, зміни статусу та видалення елементів. Бізнес-правила в результаті розкидані по обробникам подій.

Що писати замість цього

useReducer переміщує переходи стану з компонента в одну функцію. Наведений нижче редьюсер використовує дискриміновану унію для своїх дій, тож TypeScript знає тип даних для кожного випадку. При додаванні завдання додається новий об’єкт із генерованим id; при зміні статусу використовується метод map для обробки списку та зміни статусу відповідного елемента. Зверніть увагу, що у рядку зі зміною статусу є опечатка: ...todo.completed має бути !todo.completed, що є логічним запереченням, інакше код не скомпілюється.

type Action =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: string };

function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, { id: crypto.randomUUID(), text: action.payload, completed: false }];
    case 'TOGGLE_TODO':
      return state.map((todo) =>
        todo.id === action.payload ? { ...todo, completed: ...todo.completed } : todo
      );
    default:
      return state;
  }
}

Редьюсери мають переваги у двох аспектах:

  • Тестуваність. Редюсер — це чиста функція, тож її можна протестувати без відображення будь-чого чи імітації React.
  • Прогнозованість. Кожне оновлення проходить через названу дію замість довільних функцій-встановлювачів, які викликаються з різних калебеків, що полегшує відстеження та запис змін.

4. Відсутність тестів через те, що „тестування — це великий проєкт“

Багато команд ставляться до тестування як до чогось або все, або нічого: або повна налаштування від початку до кінця, що здається надто складним, або зовсім нічого. Кращий підхід полягає у тому, щоб почати з тих тестів, які найлегше написати та які мають найбільшу цінність для підтримки.

Визначайте пріоритети у такому порядку:

  • Окрема бізнес-логіка. Допоміжні функції, форматувальники, валідатори та інструменти, які знаходяться поза деревом компонентів.
  • Редюсери та користувацькі хуків. Чиста логіка переходу стану.
  • Критичні шляхи використання користувачем. Ключові взаємодії, перевірені за допомогою React Testing Library (@testing-library/react).
  • Тест на редюсер є хорошим першим кроком. Він створює початковий стан, безпосередньо надсилає дію через функцію та перевіряє результат, без необхідності відображення:

    // Example: Testing a reducer takes 2 minutes and provides huge peace of mind
    test('should toggle todo status', () => {
      const initialState = [{ id: '1', text: 'Write Article', completed: false }];
      const newState = todoReducer(initialState, { type: 'TOGGLE_TODO', payload: '1' });
    
      expect(newState[0].completed).toBe(true);
    });
    

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

    5. Використання useEffect для синхронізації React з самим собою

    useEffect — це найчастіше неправильно використовуваний хук у React. Поширеною практикою є його використання для копіювання чи коригування внутрішнього стану при зміні пропса, що спричиняє додаткове відображення та короткий період несумісності інтерфейсу.

    Постійно виникають два хибні уявлення:

    • «Стан потрібно оновлювати в ефекті, коли змінюється проп.» Зазвичай це не потрібно. Обчислюйте значення під час відображення, або, якщо стан дитини має початися з нуля, надайте йому key, який змінюється разом із відповідним пропом, щоб React перевстановив його.
    • «Логіка кліку має знаходитися в ефекті.» Логіка, яка запускається дією користувача, має бути у обробнику подій, наприклад onClick, а не в ефекті, який стежить за зміною стану, спричиненою кліком.

    Керівний принцип: ефекти є способом синхронізації з системами за межами React, такими як API браузера, підписки, інструменти аналітики чи сторонні віджети DOM. Дані, якими вже володіє React, можна трансформувати без їх використання. Для більш детального розгляду з прикладами до та після дивіться stop syncing state with useEffect.

    6. Мемоізація всього за замовчуванням

    Робота над продуктивністю часто коливається між двома крайнощами: або повне ігнорування витрат на відображення, або обгортання кожного компонента у React.memo та кожної функції у useCallback.

    Чому загальна мемоізація призводить до негативних наслідків

    useMemo та useCallback не є безкоштовними. React мусить зберігати залежності та порівнювати їх під час кожного відображення. Для компонента, який легко відображати, такий облік може коштувати стільки ж, скільки сам процес відображення, який намагаються пропустити, і це ускладнює читання коду. Мемоізація також допомагає лише тоді, коли всі пропси є стабільними; один лінійний об’єкт чи функція-стрілка, передана мемоізованому дочірньому елементу, скасовує її ефект.

    Більш ретельний підхід

    • Спочатку проаналізуйте. Профілер React DevTools показує, які компоненти насправді відображаються занадто часто або занадто повільно.
    • Виправте структуру перед додаванням мемо. Перенесення стану до компонента, який його використовує, або передача дорогих піддрощів як children часто дозволяє уникнути зайвих відображень без будь-якої мемоізації.
  • Зберігайте результати обчислень там, де це важливо. Використовуйте React.memo для великих списків та складних діаграм, а також useCallback та useMemo, щоб забезпечити стабільність їхніх параметрів.
  • 7. Сприйняття CSS та доступності як чужої роботи

    Компетентні інженери React також добре розбираються в браузерній платформі. Компоненти є настільки ж якісними, наскільки якісним є маркування та стилі, які вони створюють.

    Сфери, які варто опанувати:

    • Сучасне форматування. Добре орієнтуйтесь у Flexbox та CSS Grid, щоб абсолютне позиціонування більше не було альтернативою для звичайних макетів.
    • Сконцентроване форматування. Зберігайте стилі поруч із компонентами за допомогою CSS Modules, Tailwind CSS чи CSS-in-JS, замість того щоб покладатися на великі глобальні таблиці стилів, де імена класів конфліктують.
  • Доступність та зручність використання. Використовуйте семантичні елементи, такі як справжній <button> замість <div onClick={...}>. Переконайтеся, що навігація за допомогою клавіатури з клавішами Tab, Enter та Space працює скрізь. Надавайте видимі стилі :focus-visible та чіткі інтерактивні стани, наприклад, вказівник миші над елементами, які можна клікнути.
  • Чек-лист перевірки

    Під час перегляду запиту на пул-реквест у React шукайте такі заміни:

    • Похідні значення, збережені у додаткових гуках useState: обчислюйте їх під час відображення.
    • Ручні виклики API всередині useEffect: використовуйте TanStack Query, SWR або RTK Query.
    • Багато викликів useState для складних масивів та об’єктів: перемістіть логіку переходів у useReducer та протестуйте цей редюсер.
    • Жодних тестів, оскільки налаштування здається дорогим: почніть із чистих функцій, редюсерів та власних хуків.
    • useEffect використовується для синхронізації стану: обчислюйте значення або обробляйте його у обробнику подій.
    • useCallback та React.memo скрізь: проведіть аналіз перед мемоїзацією.
    • Елементи div, які можна натиснути, та відсутні стилі фокусу: використовуйте семантичний HTML та перевіряйте доступ через клавіатуру.

    Підсумок

    Більшість цих проблем мають спільну причину: стан чи логіка існують у багатьох місцях або виконуються частіше, ніж це необхідно. Зберігання одного джерела істини, надання спеціально створеним інструментам контролю над даними сервера, ізоляція переходів у чистих функціях та використання ефектів лише для зовнішнього світу — усе це зменшує кількість елементів, які можуть змінюватися. Код із меншою кількістю таких елементів легше тестувати, його легше оптимізувати у важливих моментах, і йому легше довіряти наступному перевірювачу.