Главная / Статьи / Шаблоны дизайна в React: от классического ООП до современных хуков

Шаблоны дизайна в React: от классического ООП до современных хуков

Объясняется, как классические шаблоны программного обеспечения, такие как Singleton, Factory и Observer, применяются в React, наряду с шаблонами, специфичными для React, такими как HOCs, Hooks и Compound Components.

1624 слов

Многие разработчики, только начинающие работать с 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

Higher Order Component, или HOC, — один из первых инструментов, предложенных React для решения проблем, связанных с множественными задачами в приложении. Представьте ситуацию, когда после того, как пользователь соглашается на отслеживание, необходимо отправить аналитическое событие сразу после загрузки компонента. Внедрение такой логики в каждый компонент страницы быстро превращается в рутинную работу, а её обновление позже — например, при изменении SDK для аналитики — становится источником проблем при техническом обслуживании. Именно для решения подобных проблем и существует паттерн HOC.

HOC берет существующий компонент и добавляет к нему дополнительное поведение, при этом оригинальный компонент не должен ничего знать об этом дополнительном поведении. Именно такое разделение является сутью этого паттерна. Например, компонент Page полностью сосредотачивается на отрисовке, в то время как его обертка withAnalytics(Page) отвечает за логику отслеживания отдельно.

В более новых кодовых базах эту функцию в основном выполняют хуки, но паттерн HOC всё ещё часто встречается в устаревших проектах на React.

Паттерн хуков

Немногие изменения повлияли на React так сильно, как хуки. Введённые в React 16.8 в 2019 году, они с тех пор стали стандартным подходом для написания большинства современных кодов на React.

Паттерн хуков позволяет разработчикам извлекать и повторно использовать логику с состоянием и побочные эффекты между компонентами с помощью обычных функций, фактически заменяя более старые подходы, такие как HOC и render props.

Композитный паттерн

Композитный паттерн позволяет группе компонентов косвенно сотрудничать над общим состоянием и логикой, без необходимости вручную передавать всё через атрибуты props. Он чаще всего встречается в сложных интерактивных элементах пользовательского интерфейса, таких как выпадающие меню, аккордеоны, вкладки и навигационные меню.

Контейнер / Паттерн представления

Этот паттерн обеспечивает четкое разделение ответственностей путем разделения функций компонента на две отдельные роли: одна занимается управлением логикой приложения, а другая — исключительно отображением интерфейса.

Компонент-контейнер отвечает за определение того, какие данные должен видеть пользователь. Он управляет состоянием, обрабатывает побочные эффекты и содержит логику приложения.

Компонент представления, напротив, занимается способом отображения этих данных. Он просто получает данные и функции-обратные вызовы через атрибуты props, никогда не изменяя сами данные.

Современное разработка с использованием React в большей степени опирается на пользовательские хуки, чем на разделение на контейнерные и представительские компоненты. Вместо того чтобы создавать отдельный контейнерный компонент только для загрузки данных, можно вынести логику загрузки в пользовательский хук и вызывать его непосредственно в том компоненте, который в нем нуждается. Это сохраняет разделение ответственностей, устраняя дополнительные уровни вложенности компонентов и повторяющийся шаблонный код.

Шаблон render props

При использовании этого шаблона функция передается в качестве свойства компоненту, что даёт этому компоненту контроль над состоянием и логикой, в то время как решение о том, что отображать, остаётся за тем, кто использует этот компонент.

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

В современном React пользовательские хуки в основном заменили функции render props, которые раньше использовались для передачи чистой логики обработки данных. Тем не менее, функции render props по-прежнему имеют смысл тогда, когда компоненту необходимо управлять всей поддеревной структурой, позволяя при этом вызывающему коду самому определять структуру маркировки. Библиотеки головногоless-интерфейсов, такие как React Aria и TanStack Table, опираются на этот подход для реализации сложного поведения — обработки доступности, управления фокусом и подобных функций — без навязывания определенного стилинга или структуры DOM.

Шаблон UI с ИИ

Это относительно недавнее добавление в список. Создание интерфейсов на основе ИИ, будь то чат-боты или более общие интеллектуальные ассистенты, требует тщательной координации между бэкенд-сервисами ИИ и реактивным слоем пользовательского интерфейса. Паттерн ИИ-интерфейса заключается по сути в подключении бэкендов больших языковых моделей к реактивным клиентским интерфейсам, чтобы они могли без проблем обрабатывать диалоговые переписки, потоковые ответы и асинхронную работу моделей.

Одной из ключевых идей здесь является разделение бэкенда и прокси-слоя от клиента. Чтобы избежать разглашения ключей API и правильно управлять вычислительной нагрузкой, все вызовы, связанные с ИИ, должны проходить через серверную часть — что-то вроде обработчиков маршрутов Next.js или прокси API Node.js, расположенного перед Vite. Прямой вызов сервисов ИИ из браузера — это то, чего следует полностью избегать.

Связанные материалы