WebMriya
Назад до статей

Як не тягнути API-клієнти в React-компоненти

Практична React-архітектура для розділення API-клієнтів, серверного стану та поведінки UI, поки асинхронний код не перетворив компоненти на крихкі модулі.

React
TypeScript
Frontend Architecture
Software Architecture
W
Frontend Architecture & Legacy Modernization
11 жовтня 2026 р. · 5 хв читання

У міру зростання React-продукту з’являється знайома проблема: компонент, який починався як простий екран, тепер імпортує API-клієнт, формує параметри запиту, керує прапорцями завантаження та помилки, перетворює відповідь, показує сповіщення і вирішує, що саме рендерити. Він усе ще працює, але кожна нова вимога робить його складнішим для тестування і легшим для поломки.

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

Що і де має знаходитися

Корисно сприймати API-клієнт як деталь інфраструктури, query або mutation hook — як адаптер серверного стану, а компонент — як місце, де приймаються UI-рішення. Компонент має знати, які дані йому потрібні і яку дію може виконати користувач. Йому не потрібно знати, чи дані отримані через fetch, Axios, GraphQL або локальну заглушку для розробки.

ВідповідальністьНалежне місце
HTTP-метод, URL, заголовки, парсинг відповідіAPI-клієнт
Кешування, повторні запити, refetch, статус запитуQuery або mutation hook
Специфічне для домену перетворення і валідаціяДоменний адаптер
Видимість, disabled-стани, порожні станиReact-компонент
Сповіщення і навігація після діїFeature-level hook або компонент

Це не означає, що для кожного endpoint потрібно створювати по п’ять файлів. Межа корисна тоді, коли вона прибирає знання, які інакше дублювалися б у різних екранах. Невелика feature може використовувати одну функцію клієнта і один hook. Для більшої feature можна додати доменний адаптер, якщо дані бекенду не відповідають формі, яку має споживати UI.

Почніть із вузького API-контракту

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

ts
export type Project = {
  id: string;
  name: string;
  status: "active" | "archived";
};

export async function getProject(projectId: string): Promise<Project> {
  const response = await fetch(`/api/projects/${projectId}`);

  if (!response.ok) {
    throw new Error("Unable to load project");
  }

  return response.json() as Promise<Project>;
}

Приклад навмисно невеликий, але він встановлює важливий контракт: викликачі не формують URL і не перевіряють HTTP-статуси. У production на цій межі зазвичай варто додати runtime-валідацію, адже типи TypeScript зникають, коли запит доходить до сервера. Про цю проблему детальніше йдеться у статті Runtime-валідація на API-межах.

Використовуйте hooks як адаптери, а не як другі компоненти

Feature hook має надавати серверний стан у формі, яку може спожити екран. Він може вибрати query key, налаштувати stale time, перетворити помилки і надати mutation-функції. Водночас йому не варто накопичувати несуміжні UI-рішення, наприклад відкривати модальне вікно або змінювати п’ять локальних прапорців.

ts
import { useQuery } from "@tanstack/react-query";
import { getProject } from "./project-api";

export function useProject(projectId: string) {
  return useQuery({
    queryKey: ["project", projectId],
    queryFn: () => getProject(projectId),
    enabled: projectId.length > 0,
  });
}

Тепер компонент може зосередитися на станах, які розпізнає користувач: завантаження, недоступність, порожній стан, готовність і повторна спроба. Йому не потрібні окремі effects для синхронізації відповіді з локальним state, оскільки query вже керує життєвим циклом серверних даних. Для flow з великою кількістю полів такий самий поділ допомагає відокремити стан полів від серверних помилок; схожий підхід описано у статті React-форми без розростання state.

Не копіюйте серверний стан у локальний без потреби

Поширений рефакторинг додає query, а потім одразу копіює його результат у useState. Так з’являються два джерела правди. Коли фоновий refetch повертає новіші дані, локальна копія може залишитися застарілою. Коли mutation завершується успішно, компонент може оновити одну копію, тоді як кеш усе ще міститиме іншу.

  • Зберігайте отримані дані в query cache, якщо тільки користувач не редагує тимчасовий draft.
  • Використовуйте локальний state для стану взаємодії: відкритої панелі, вибраного рядка або незбереженого input.
  • Після відправлення draft інвалідуйте або оновлюйте відповідний запис серверного стану, а не синхронізуйте вручну кожен екран.
  • Якщо відповідь потрібно перетворити, зробіть це явно і протестуйте окремо.

Зробіть обробку помилок змістовною

API-клієнт має викидати помилку, яка зберігає достатньо контексту для рішення на наступному рівні. Компонент не повинен відрізняти 401, помилку валідації, timeout і збій сервера, розбираючи рядок на кшталт «Request failed». Водночас низькорівневі транспортні деталі не мають просочуватися в кожну кнопку.

Практична форма помилки може містити стабільний code, HTTP status і необов’язкові помилки полів. Feature hook може перетворити ці значення на невеликий набір UI-результатів: перенаправити на sign-in, показати inline-валідацію, запропонувати повторити запит або вивести загальне повідомлення про збій. Так поведінка відновлення стає свідомою, а не розкиданою по render-логіці.

Тестуйте межу, якою ви справді володієте

Коли компоненти напряму викликають конкретний API-клієнт, тести часто мусять одночасно мокати fetch, таймери, сповіщення, навігацію і кілька деталей реалізації. Вужчий контракт hook дозволяє component-тестам отримувати контрольовані дані й зосереджуватися на рендерингу та взаємодії. Тести API-клієнта окремо перевіряють формування URL, парсинг і перетворення помилок.

  1. Тестуйте API-функції на репрезентативних успішних відповідях і відповідях з помилками.
  2. Тестуйте доменні адаптери на неповних або некоректних payload.
  3. Тестуйте hooks для query keys, умов enabled і побічних ефектів mutations.
  4. Тестуйте компоненти на видимі для користувача стани та дії, а не на внутрішню механіку запитів.

Коли такий поділ справді виправданий

Не перетворюйте невелику сторінку на архітектурну вправу. Прямий виклик може бути нормальним для короткоживучого внутрішнього екрана з одним запитом без повторного використання. Поділ стає цінним, коли одні й ті самі дані використовують кілька компонентів, важливе кешування, контракт бекенду нестабільний, feature має optimistic updates або команда хоче змінити транспортний шар без переписування UI.

Хороше правило: переносіть код через межу, коли одне й те саме знання з’являється більш ніж в одному місці. Якщо три компоненти знають форму endpoint, централізуйте її. Якщо два екрани реалізують однакову retry-поведінку, винесіть адаптер. Якщо hook починає вирішувати питання layout або поведінки modal, поверніть це рішення ближче до UI feature.

Результат — менш крихкий React

Не тягнути API-клієнти в React-компоненти — це не про модну структуру папок. Це про контроль змін. Транспортні деталі можуть еволюціонувати без переписування екранів, поведінку серверного стану можна тестувати без рендерингу сторінки, а компоненти можуть представляти саме ті стани, які бачать користувачі.

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

Поширені запитання

Чи повинен кожен API-запит мати власний hook?

Ні. Створюйте hook, коли операція має повторно використовувану поведінку серверного стану, кешування, lifecycle-правила або специфічне для feature перетворення. Одноразовому запиту на невеликому екрані додаткова абстракція може бути не потрібна.

Чи може React-компонент напряму викликати fetch?

Може, але прямі виклики стають дорогими, коли логіку запиту потрібно повторно використовувати або екрану потрібні кешування, retry та однакова обробка помилок. Типізована API-функція прибирає ці деталі з render-коду.

Де має відбуватися валідація відповіді?

Валідуйте зовнішні дані на API-межі, перш ніж вони поширяться застосунком. Після цього решта feature може працювати з перевіреною доменною формою.