Головна / Статті / Шаблони дизайну React: від класичного OOP до сучасних хуків

Шаблони дизайну React: від класичного OOP до сучасних хуків

Пояснює, як класичні шаблони програмного забезпечення, такі як Singleton, Factory та Observer, застосовуються в React, а також шаблони, специфічні для React, як-от HOCs, Hooks та Compound Components.

1624 слів

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

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

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

Класичні шаблони інженерії програмного забезпечення

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

Шаблон Singleton

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

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

// Singleton API Service
class APIClient {
  constructor() {
    if (APIClient.instance) {
      return APIClient.instance;
    }
    this.baseURL = "https://api.example.com";
    APIClient.instance = this;
  }

  fetchData(endpoint) {
    return fetch(`${this.baseURL}${endpoint}`).then(res => res.json());
  }
}

// Any module importing/instantiating this gets the exact same instance
const client1 = new APIClient();
const client2 = new APIClient();
console.log(client1 === client2); // true

За допомогою сучасних ES-модулів більше не потрібна логіка ручної перевірки екземпляра — достатньо просто експортувати один спільний об’єкт чи екземпляр, щоб отримати поведінку singleton без додаткових зусиль.

// apiClient.js
export const apiClient = new APIClient(); // ES modules cache exports automatically

Шаблон фабрики

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

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

// Button Factory for dynamically rendering UI elements
function createButton(type, config) {
  switch (type) {
    case 'primary':
      return { role: 'btn-primary', label: config.label, onClick: config.onClick };
    case 'icon':
      return { role: 'btn-icon', icon: config.iconName, onClick: config.onClick };
    case 'link':
      return { role: 'btn-link', href: config.url };
    default:
      throw new Error(`Unsupported button type: ${type}`);
  }
}

const primaryBtn = createButton('primary', { label: 'Submit', onClick: () => {} });

Схема спостерігача

Це основний механізм, який лежить в основі слухачів подій та бібліотек керування станом, таких як Redux, Zustand та MobX. Варто зазначити, що API Context у React не ґрунтується на цій схемі.

class EventEmitter {
  constructor() {
    this.events = {};
  }

  // Subscribe
  on(event, listener) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(listener);
  }

  // Publish
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(listener => listener(data));
    }
  }
}

// Usage
const store = new EventEmitter();

// Component A subscribes to state changes
store.on('userLoggedIn', user => console.log(`Welcome, ${user.name}!`));

// Login Service triggers event
store.emit('userLoggedIn', { name: 'Sarah' });

Схема модуля

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

До появи нативних модулів ES6 це була стандартна техніка для підтримання чистоти глобального об’єкта window та забезпечення справжньої приватності змінних у JavaScript.

// Module using IIFE (Immediately Invoked Function Expression)
const ShoppingCartModule = (function () {
  // Private variable
  const cart = [];

  // Private function
  function calculateTotal() {
    return cart.reduce((sum, item) => sum + item.price, 0);
  }

  // Public API
  return {
    addItem(item) {
      cart.push(item);
    },
    getTotal() {
      return calculateTotal();
    }
  };
})();

ShoppingCartModule.addItem({ name: 'Keyboard', price: 100 });
console.log(ShoppingCartModule.getTotal()); // 100
console.log(ShoppingCartModule.cart); // undefined (Private!)

Сьогодні нативні ES-модулі з функціями import та export автоматично керують областями видимості, тому рідко потрібно вручну створювати замикання лише для того, щоб зробити змінні приватними.

// cart.js
const cart = []; // Private to cart.js file
export const addItem = (item) => cart.push(item);
export const getTotal = () => cart.reduce((sum, i) => sum + i.price, 0);

Специфічні шаблони проектування компонентів у React

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

У цьому розділі розглядаються наступні шаблони на рівні компонентів:

  • шаблон HOC
  • шаблон, побудований на основі хуків
  • шаблон складних компонентів
  • розділення на контейнери та компоненти візуалізації
  • підхід з використанням render props
  • новий шаблон UI для ШІ

Шаблон HOC

Компонент вищого порядку, або HOC, є однією з перших технік, які запропонував React для вирішення проблем, що стосуються кількох компонентів одночасно. Уявімо ситуацію, коли після того, як користувач погоджується на відстеження, необхідно відправити подію аналітики як тільки компонент завантажується. Вбудовування цієї логіки в кожен компонент сторінки швидко стає повторюваним процесом, а її оновлення пізніше — наприклад, коли змінюється SDK аналітики — перетворюється на проблему технічного обслуговування. Саме для вирішення таких проблем існує паттерн HOC.

HOC бере існуючий компонент та додає до нього додаткову поведінку, при цьому оригінальний компонент не потребує жодного усвідомлення цієї додаткової поведінки. Саме це розділення є суттю паттерну. Наприклад, компонент Page залишається зосередженим виключно на відображенні, тоді як його обгортання через withAnalytics(Page) забезпечує обробку логіки відстеження окремо.

У новіших кодових базах цю функцію переважно виконують хуки, але у старих проектах на React ви все ще часто натрапляєте на шаблон HOC.

Шаблон хуків

Мало що змінило React настільки фундаментально, як хуки. Вони були введені у React 16.8 у 2019 році та з того часу стали стандартним підходом для написання більшості сучасного коду на React.

Шаблон хуків дозволяє розробникам виймати та повторно використовувати логіку зі станом та побічні ефекти між компонентами за допомогою звичайних функцій, ефективно замінюючи старіші підходи, такі як HOC та render props.

Композитний шаблон

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

Контейнер / Шаблон представлення

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

Компонент-контейнер відповідає за визначення тих даних, які має бачити користувач. Він керує станом, обробляє побічні ефекти та містить логіку додатку.

Натомість компонент представлення зосереджується на тому, як саме відображаються ці дані. Він просто отримує дані та функції-відповіді через атрибути props, ніколи не змінюючи самі основні дані.

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

Шаблон render props

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

Основна ідея render props полягає у тому, що замість того, щоб сам компонент-обгортка відображав жорстко задану інтерфейсну структуру, він виконує свою внутрішню логіку, а потім викликає функцію-властивість для створення відповідного JSX.

У сучасному React користувацькі хуки переважно взяли на себе роль render props, які раніше використовувалися для передачі чистої логіки даних. Проте render props все ще мають сенс, коли компоненту потрібно керувати цілим піддревом, залишаючи водночас можливість тому, хто його викликає, вирішувати питання форматування. Безголові бібліотеки UI, такі як React Aria та TanStack Table, використовують цей підхід для реалізації складної поведінки — обробки доступності, керування фокусом та подібних аспектів — не нав’язуючи жодного конкретного стилізування чи структури DOM.

Шаблон UI для ШІ

Це відносно новий елемент у цьому списку. Створення інтерфейсів на основі ШІ, чи то чат-ботів, чи більш загальних інтелектуальних асистентів, вимагає ретельної координації між бекенд-сервісами ШІ та реактивним шаром користувацького інтерфейсу. Патерн AI UI полягає по суті у підключенні бекендів з великими мовними моделями до реактивних клієнтських інтерфейсів, щоб вони могли без проблем обробляти діалогові переписки, потокові відповіді та асинхронну роботу моделей.

Однією з ключових ідей тут є розділення бекенду та проксі-шару від клієнта. Щоб уникнути розкриття ключів API та належним чином керувати обчислювальним навантаженням, усі виклики, пов’язані з ШІ, мають проходити через серверний шар — щось на кшталт обробників маршрутів Next.js або проксі API Node.js, розташованого перед Vite. Прямий виклик сервісів ШІ з браузера — це те, чого слід повністю уникати.

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