Головна / Статті / Три шаблони TypeScript, які покращують архітектуру React-додатку

Три шаблони TypeScript, які покращують архітектуру React-додатку

Дізнайтеся, як шаблони Repository, Observer та Builder використовують систему типів TypeScript для створення більш чистих та легкозберіганих кодових баз React та Next.js.

763 слів

Написання коду на TypeScript не означає його ефективного використання

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

Після роботи над продакшн-додатками з React та Next.js я помітив кілька паттернів, які справді змінюють ситуацію. Це не абстрактні вправи з підручників — це рішення проблем, з якими ви дійсно стикнетеся.

1. Паттерн репозиторію — відокремте отримання даних від усього іншого

Коли ваші компоненти безпосередньо викликають fetch(), ви створюєте умови для складного переписування коду, як тільки API змінюється.

Паттерн репозиторію обгортає доступ до даних за допомогою чистого інтерфейсу:

// repositories/userRepository.ts
interface UserRepository {
  getById(id: string): Promise<User>;
  getAll(): Promise<User[]>;
}

export class ApiUserRepository implements UserRepository {
  async getById(id: string): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  }

  async getAll(): Promise<User[]> {
    const res = await fetch('/api/users');
    return res.json();
  }
}

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

2. Патерн спостерігача — реактивний стан без зайвої ваги Redux

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

// utils/eventBus.ts
type EventMap = {
  'user:loggedIn': { userId: string };
  'cart:updated': { itemCount: number };
};

class TypedEventBus {
  private listeners: Partial<{
    [K in keyof EventMap]: ((payload: EventMap[K]) => void)[]
  }> = {};

  on<K extends keyof EventMap>(event: K, cb: (payload: EventMap[K]) => void) {
    (this.listeners[event] ??= []).push(cb);
  }

  emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
    this.listeners[event]?.forEach(cb => cb(payload));
  }
}

export const eventBus = new TypedEventBus();

Усе тут має суворо визначені типи — жодних типів any, які ховаються в тіні. Це означає, що немає неоднозначностей щодо того, яку форму даних має очікувати певний слухач.

3. Патерн будівельника — надання порядку у створенні складних об’єктів

Якщо ви коли-небудь стикалися зі складнощами під час створення об’єктів-фільтрів, конфігурацій запитів API чи схем форм, наповнених вкладеними умовами, то шаблон Builder негайно принесе ясність:

// builders/queryBuilder.ts
class QueryBuilder {
  private params: Record<string, string> = {};

  withPage(page: number) {
    this.params['page'] = String(page);
    return this;
  }

  withLimit(limit: number) {
    this.params['limit'] = String(limit);
    return this;
  }

  withSearch(term: string) {
    if (term.trim()) this.params['q'] = term;
    return this;
  }

  build(): string {
    return new URLSearchParams(this.params).toString();
  }
}

// Usage
const query = new QueryBuilder()
  .withPage(1)
  .withLimit(20)
  .withSearch('typescript')
  .build();
// → "page=1&limit=20&q=typescript"

Отриманий код легко читається, його елементи чітко поєднуються між собою, і структурно неможливо виконувати кроки у неправильній послідовності.

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

  • Шаблон Repository: відокремлює шар доступу до даних від логіки користувацького інтерфейсу, дотримуючись принципу інверсії залежностей
  • Шаблон Observer: дозволяє здійснювати легку, реактивну комунікацію між компонентами додатку без зайвого коду
  • Шаблон Builder: робить створення складних об’єктів одночасно зрозумілим та безпечним
  • Усі три шаблони значно виграють від інтерфейсів та генериків TypeScript, що робить їх значно чистішими порівняно з аналогами у звичайному JavaScript

«TypeScript — це не просто додавання типів; він створює взаємодію між кожним шаром вашого додатку». — Ден Вандеркам, автор книги Effective TypeScript

Що спробувати далі

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

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

Пов’язана література

  • Коли ШІ пише ваш React-додаток, але ігнорує принципи чистого коду — Дізнайтеся про сім звичок чистого коду — DRY, принцип єдиної відповідальності, захисні клози та інші — які часто порушуються у коді React, створеному ШІ, та як їх виправити.
  • Розуміння React Lanes: як бітмаски кодують пріоритет оновлень — Дізнайтеся, як React використовує бітмаски для кодування кількох пріоритетів оновлень у одне ціле число, та чому для планування використовуються бітові операції замість простих булевих флагів.