Шаблоны дизайна в React: от классического ООП до современных хуков
Объясняется, как классические шаблоны программного обеспечения, такие как Singleton, Factory и Observer, применяются в React, наряду с шаблонами, специфичными для React, такими как HOCs, Hooks и Compound Components.
Многие разработчики, только начинающие работать с 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. Прямой вызов сервисов ИИ из браузера — это то, чего следует полностью избегать.
Связанные материалы
- Три шаблона TypeScript, улучшающих архитектуру React-приложений — Узнайте, как шаблоны Repository, Observer и Builder используют систему типов TypeScript для создания более чистых и удобных в обслуживании кодовых баз для React и Next.js.
- Типизация React-хуков: useState, useEffect, useReducer и пользовательские хуки — Узнайте, как правильно типизировать useState, useEffect, useReducer и пользовательские хуки в TypeScript, а также когда использование TypeScript вместо обычного JavaScript действительно оправдано.