Архитектура чистых компонентов в React

Никита Калашников
В разработке веб-приложений ключевым фактором поддерживаемости является чистота компонентов. Предсказуемость интерфейса напрямую зависит от того, насколько строго вы соблюдаете разделение ответственности.
Чистые компоненты — это функции, которые возвращают одинаковый UI для одинаковых входных данных (props) и не имеют побочных эффектов.
Однако на практике разработчики часто сталкиваются с проблемой смешивания бизнес-логики и отображения, что превращает проект в поддерживаемое легаси.
Проблемы смешивания логики и UI
Главная ошибка — создание монолитных компонентов, которые знают слишком много. Рассмотрим реалистичный сценарий: кнопка для ввода промокода, которая используется на разных страницах акций.
1// Плохо: Логика зашита в UI
2function PromoButton({ campaignId }) {
3 const [open, setOpen] = useState(false);
4
5 const handleSubmit = async (code) => {
6 // Прямой вызов API внутри компонента
7 const res = await fetch(`/api/apply/${campaignId}`, { body: code });
8
9 // Жесткая логика уведомления
10 if (res.ok) {
11 toast.success('Промокод активирован!');
12 setOpen(false);
13 }
14 };
15
16 return (
17 <button onClick={() => setOpen(true)}>Ввести промокод</button>
18 {open && <Form submit={handleSubmit} />}
19 );
20}
Чем это обернется:
- Невозможность переиспользования. На странице «Черная пятница» требуется toast-уведомление (как в коде). На странице «Новый год» маркетинг требует показать модальное окно с кнопкой «Принять подарок». Вам придется копировать компонент и менять в нем логику, нарушая принцип DRY.
- Сложность тестирования. Чтобы протестировать кнопку, нужно мокать
fetchи проверять вызовыtoast. UI-тесты становятся хрупкими и зависят от внешней среды. - Нарушение SOLID. Компонент нарушает принцип единственной ответственности (SRP). Он отвечает и за верстку, и за запросы, и за бизнес-сценарии успеха.
Правильная архитектура: Разделение слоев
Чтобы чистые компоненты работали эффективно, нужно выделить три уровня абстракции:
- Service (API): Чистая функция запроса, не зависящая от React.
- UI Component: Глупая форма, которая только отдает события (onSubmit, onClose).
- Logic Container: Связывает сервис и UI, обрабатывая специфичные сценарии.
1. Слой данных (Service)
1// services/promoService.js
2export const applyPromo = async (campaignId, code) => {
3 const res = await fetch(`/api/apply/${campaignId}`, { body: code });
4 return res.json();
5};
2. Чистый UI компонент
1// components/PromoForm.jsx
2function PromoForm({ isOpen, onClose, onSubmit, isLoading }) {
3 if (!isOpen) return null;
4 return (
5 <Modal onClose={onClose}>
6 <InputForm onSend={onSubmit} disabled={isLoading} />
7 </Modal>
8 );
9}
3. Реализация на разных страницах Теперь мы используем одну форму с разной логикой обработки успеха.
Страница А (Toast уведомление):
1function BlackFridayPage() {
2 const handleSubmit = async (code) => {
3 await applyPromo('bf2024', code);
4 toast.success('Успех!'); // Логика страницы А
5 };
6 return <PromoForm isOpen={open} onSubmit={handleSubmit} />;
7}
Страница Б (Модальное окно успеха):
1function NewYearPage() {
2 const handleSubmit = async (code) => {
3 await applyPromo('ny2024', code);
4 setSuccess(true); // Логика страницы Б
5 };
6 return (
7 <>
8 <PromoForm isOpen={open} onSubmit={handleSubmit} />
9 {success && <GiftModal />}
10 </>
11 );
12}
Преимущества подхода
Такая разработка гарантирует стабильность. Если изменится API, правится только promoService. Если изменится дизайн формы, правится только PromoForm. Если изменится сценарий награды, правится только страница-контейнер.
- Тестируемость: Чистый UI легко проверить на снепшотах. Логика хука тестируется отдельно без рендеринга.
- Производительность: Чистые компоненты легче оптимизировать через
React.memo, так как их ререндер зависит только от props. - Масштабируемость: Декомпозиция по SOLID позволяет изолировать изменения. Правка верстки не сломает логику запросов.
Итог
Использование чистых компонентов в React — это необходимость для масштабирования. Не заставляйте кнопки знать о бизнес-процессах, они должны просто вызывать события. Правильная декомпозиция снижает когнитивную нагрузку на команду и ускоряет разработку в долгосрочной перспективе.