Ciao, amici!

На связи Томас Анджело Игорь, frontend-разработчик.
Вы когда-нибудь бывали в городе Лост-Хэвен? Это город, что никогда не спит. Город, где неоновые вывески зданий отражаются в лужах после дождя, а в темном переулке стреляют из автомата «Томпсона». Этим городом правят криминальные семьи, где у каждой свои интересы, и когда эти интересы пересекаются, город погружается в хаос.
Этот город растет и развивается как и… приложения которые мы пишем на React. И очень хорошо, если разработчик, закладывая фундамент, думает о будущем: строит системы, которые можно переиспользовать, тестировать и расширять. Когда о будущем подумать забываем, получаем технический долг, который в итоге придётся разгребать всей команде. Как в большом городе: за архитектурные ошибки платят все.
В этой статье разберем, какие паттерны еще держат оборону, какие канули в лету, а какие переродились во что-то новое. Покажу опасные районы и расскажу, как не угробить проект.
Садитесь поудобнее. Бензина полно, а ночь только начинается.
Мы выезжаем. 🚬🚕
Город, который построил ты
Прежде чем мы начнём разбирать паттерны, давайте поговорим о том, от чего они нас спасают.
В каждом проекте рано или поздно появляется компонент, у которого слишком много власти. Он управляет данными, состоянием, рендерингом, аналитикой, обработкой событий. Он знает все, решает все и… медленно убивает ваш проект, затягивая его в болото технического долга.
«Дон Сальери контролирует порт. Все, что приходит в город, проходит через него. Он считает себя королём Лост-Хэвена»
God Component (почти что Godfather) — это антипаттерн React, который нарушает Принцип единственной ответственности (Single Responsibility Principle) из SOLID.
Такой компонент аккумулирует слишком много обязанностей: сложное управление состоянием, API-запросы и презентационную логику. Из-за этого код становится нечитаемым, а компонент невозможно переиспользовать, потому что он слишком привязан к конкретному контексту. Найти что-то в файле на 300+ строк — тот еще квест. Тестировать его тоже мало приятного: любое изменение с высокой вероятностью может что-то поломать.
Решая проблему God Component мы декомпозируем компонент, по итогу получая более понятный код. Но тут же получаем новую проблему: пропс-дриллинг.
Эта проблема тянет за собой увеличение количества шаблонного кода, при этом логика начинает размазываться по всему приложению. Так создаётся сильная связанность между компонентами. Если в самом верху меняется объект, React перерендеривает каждый компонент в цепочке, из-за чего может падать производительность.
Использование глобального стейта на первый взгляд проблемой не кажется. Это ведь удобно: открываешь Redux-стор, и вот оно, все состояние приложения. Но проблема в том, что невозможно сказать наверняка, где именно используется глобальный стейт.
«В этом городе влияние мафии чувствуется везде. Но чем больше власти, тем сложнее понять, кто на кого работает»
Меняем одно поле, а после этого падает виджет на соседней странице. Или разные компоненты видят разные версии одного и того же состояния.
И последний неприятный персонаж — чрезмерное злоупотребление useEffect. Когда эффектов много, сложно предсказать, в каком порядке они выполняются при монтировании и обновлении компонента. Побочные эффекты для изменения состояния делают логику запутанной, и это при том, что еще и за синхронизацией нужно следить.

Однако у каждой проблемы есть решение. Паттерны проектирования используются как стратегии, которые помогают держать код под контролем. Они не дадут вашему приложению превратиться в неопознаваемую кучу кода и помогут выстроить архитектуру, которая выдержит любые изменения.
Итак, давайте разбираться!
Old, but gold
Начнем с паттернов, которые заложили основу современной фронтенд-разработки. В чистом виде их уже почти не встретишь, но понимать их важно как минимум для того, чтобы понимать, почему сейчас делают иначе.
1. Компоненты презентации и контейнера (Presentational and Container Components)
Популяризатор: Дэн Абрамов, 2015 год.
Суть: Разделение ответственности.
«Умный» контейнер тащит логику, стейт и запросы к API, т.е. отвечает на вопрос «Как работает компонент?».
«Глупый» презентационный компонент получает всё через пропсы и рисует интерфейс, отвечая на вопрос «Как компонент выглядит?».
Разберем этот паттерн на примере карточки пользователя:
const UserCard = ({ userId }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // ... логика useEffect для получения данных по userId }, [userId]); const handleEdit = () => { // ... логика открытия модального окна }; if (loading) return <div>Загрузка...</div>; return ( <div> <h3>{user.name}</h3> <span>{user.email}</span> <button onClick={handleEdit}>Редактировать</button> </div> );};
Вроде бы ок, но компонент делает слишком много: он и данные тащит, и состоянием управляет, и интерфейс рисует. Это нарушает принцип единой ответственности.
Применив этот паттерн, мы создадим два компонента, где каждый будет иметь свою зону ответственности.
Презентационный компонент (только UI):
const PresentationalUserCard = ({ user, onEdit }) => ( <div> <h3>{user.name}</h3> <span>{user.email}</span> <button onClick={onEdit}>Редактировать</button> </div>);
Контейнерный компонент (только стейт и логика):
const UserCardContainer = ({ userId }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // ... логика useEffect для получения данных по userId }, [userId]); const handleEdit = () => { // ... логика открытия модального окна }; if (loading) return <div>Загрузка...</div>; return <PresentationalUserCard user={user} onEdit={handleEdit} />;};
Благодаря разделению ответственности мы в любой момент можем изменить визуальную часть, не затронув при этом логику.
Что же касается актуальности этого паттерна, тут есть нюанс. Сам Дэн Абрамов в 2019 году опубликовал обновление к своей статье про него:
«Я написал эту статью очень давно, и мои взгляды с тех пор эволюционировали. В частности, я больше не рекомендую разделять компоненты таким образом. Хуки делают то же самое без произвольного разделения. Этот текст оставлен для истории, но не воспринимайте его слишком серьезно»
И действительно, сегодня мы вынесем логику получения данных пользователя в кастомный хук, например, useUser. Это сделает код более читаемым и лаконичным:
// 1. Выделение логики в кастомный хукconst useUser = (userId) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { // ... логика useEffect для получения данных по userId }, [userId]); const handleEdit = () => { // ... логика открытия модального окна }; return { user, loading, handleEdit };};// 2. Использование кастомного хука в компонентеconst UserCard = ({ userId }) => { const { user, loading, handleEdit } = useUser(userId); if (loading) return <div>Загрузка...</div>; return ( <div> <h3>{user.name}</h3> <span>{user.email}</span> <button onClick={handleEdit}>Редактировать</button> </div> );};
Этот паттерн сейчас скорее исторический артефакт, который показывает, как мыслили раньше. Он все еще важен для понимания эволюции React, но в чистом виде практически не используется.
2. Компоненты высшего порядка (Higher-Order Components, HOC)
Как появились: пришли из мира функционального программирования, будучи вдохновленными концепцией функций высшего порядка из математики.
Суть: функция, которая принимает компонент и возвращает новый компонент с расширенной функциональностью, его улучшенную версию.
Раньше этот паттерн использовался часто. Один из вариантов его использования — обработка состояния загрузки. Допустим, у нас есть компоненты, в которых нам приходится каждый раз проверять состояние загрузки (или ошибки). Мы получаем проблему дублирования кода:
const MyComponent = () => { const [isLoading, setIsLoading] = useState(true); const [data, setData] = useState([]); // ... логика useEffect для получения данных и переключения isLoading return isLoading ? "Загрузка..." : <TodoList data={data} />;};
Используя паттерн HOC создадим «фабрику» withLoading, которая будет обрабатывать состояние загрузки для любого компонента на основе переданных пропсов:
const withLoading = (Component) => (props) => { if (props.loading) return "Загрузка..."; return <Component {...props} />;};const TodoList = withLoading(BaseTodoList);// Обновленный компонентconst MyComponent = () => { const [isLoading, setIsLoading] = useState(true); const [data, setData] = useState([]); // ... логика useEffect return <TodoList loading={isLoading} data={data} />;};
Казалось бы, все круто, но по мере роста приложения мы увеличиваем количество таких «оберток». Например, проверяем права доступа, настраиваем тему, переводы, логируем данные или отлавливаем ошибки. Мы начинаем вкладывать вызовы HOC и как итог попадаем в наш персональный котел — Wrapper Hell.
const EnhancedComponent = withErrorHandler( withLoading( withAuthorization( withDataFetching( withLogging( withTheme( withIntl( withRouting(MyComponent) ) ) ) ) ) ));
Сегодня HOC тоже вытеснили хуки. По той же причине, что и контейнеры. Плоская структура без вложенности читается несравнимо легче.
Вместо того, чтобы оборачивать компонент по 10 раз, мы берем данные из кастомного хука внутри компонента, получая плоскую структуру без лишних оберток и возможных конфликтов:
const GreetingComponent = (props) => { const { user, loading, error } = useUser(); if (loading) return <Loading />; if (error) return <ErrorMessage error={error} />; return <div>Привет, {user.name}</div>;};
3. Пропсы рендеринга (Render Props)
Популяризаторы: Майкл Джексон и Кент С. Доддс , 2017 год.
Суть: компонент принимает функцию в пропс, которая возвращает React-элементы. Компонент управляет состоянием, а вы решаете, как его отрисовать.
Применяя этот паттерн, получаем хорошую гибкость. Вместо того чтобы жестко задавать, что именно рендерить, компонент дает данные, а мы уже сами решаем, как их стилизовать. То есть, принимаем стратегии отрисовки дочерних компонентов через пропсы, что позволяет инкапсулировать состояние. К минусам использования такого подхода можно отнести сложность в понимании для начинающих разработчиков.
Рассмотрим применение паттерна на примере модального окна. Состояние isOpen живет в компоненте-обертке, а потребитель должен иметь возможность управлять этим состоянием (открыть/закрыть). Для этого мы передаем функцию через пропс renderContent. Эта функция получает все необходимое и возвращает разметку:
// 1. Создаем компонент-обертку, который будет управлять состоянием модального окнаconst Modal = ({ renderContent }) => { const [isOpen, setIsOpen] = useState(false); const open = () => setIsOpen(true); const close = () => setIsOpen(false); return renderContent({ isOpen, open, close });};// 2. Использование — передаем функцию в проп renderContent<Modal renderContent={({ isOpen, open, close }) => ( <> <button onClick={open}>Открыть модальное окно</button> {isOpen && ( <div role="dialog"> <p>Это модальное окно</p> <button onClick={close}>Закрыть</button> </div> )} </> )}/>
Компонент Modal управляет состоянием и предоставляет интерфейс для управления, но полностью делегирует отрисовку потребителю. Логика открытия/закрытия не дублируется в каждом месте использования, тем самым исключая пропс-дриллинг.
Рассмотрим применение паттерна Render Props на примере модального окна. Состояние isOpen и базовая логика (open/close) живут внутри Modal. Также сам Modal отвечает за доступность и «каркас окна». Потребитель передает две функции: renderTrigger (то, что открывает окно) и renderContent (что показывать внутри):
// 1. Компонент-обертка с инкапсулированной логикой const Modal = ({ renderTrigger, renderContent, closeLabel = 'Закрыть' }) => { const [isOpen, setIsOpen] = useState(false); const open = () => setIsOpen(true); const close = () => setIsOpen(false); return ( <> {renderTrigger({ open })} {isOpen && ( <div role="dialog" aria-modal="true" className="modal"> <button className="modal-close" onClick={close}> {closeLabel} </button> {renderContent({ close })} </div> )} </> );};// 2. Использование — триггер и контент рендерятся отдельноconst App = () => ( <Modal renderTrigger={({ open }) => ( <button onClick={open}>Открыть модальное окно</button> )} renderContent={({ close }) => ( <> <p>Это модальное окно</p> <button onClick={close}>Ок</button> </> )} />);
Modal управляет состоянием, доступностью и структурой окна, а потребитель полностью контролирует внешний вид trigger и содержимого через render props. Логика открытия/закрытия не дублируется по приложению, а повторно используется через единый контракт компонента.
На сегодняшний день паттерн Render Props ценен в сценариях, где важно вложить разметку внутрь, сохранив доступ к данным контейнера. Например, когда две фичи архитектурно изолированы друг от друга, и одна встраивается в другую, но при этом ей нужны данные из родительского контекста. В таких случаях Render Props позволяет избежать лишних связей и дублирования логики.
Далее рассмотрим два паттерна, которые неразрывно связаны и являются вариациями одного подхода к управлению пропсами в переиспользуемых компонентах и хуках.
4. Коллекции пропсов и геттеры пропсов (Prop Collection, Prop Getter)
Популяризатор: Кент С. Доддс, создавая библиотеку Downshift, 2017 год.
Суть: группируем связанные пропсы в одну коллекцию или геттер-функцию.
Идея паттерна коллекции пропсов в том, что компонент или хук возвращает готовый объект с набором необходимых пропсов, который в последствии мы применяем к DOM-элементу. Мы берем этот объект и разворачиваем его через {...collection}.
Допустим, нам нужно передать сразу много атрибутов. Вот как хук useToggle возвращает коллекцию для кнопки-переключателя:
// 1. Выносим все пропсы для кнопки в объект buttonPropsconst useToggle = () => { const [on, setOn] = useState(false); const toggle = () => setOn(prev => !prev); const buttonProps = { role: 'button', onClick: toggle, className: 'toggle-button', 'aria-pressed': on, }; return { on, buttonProps };}// 2. “Распыляем” этот объект в компонентеconst ToggleButton = () => { const { on, buttonProps } = useToggle(); return ( <button {...buttonProps}> {on ? 'Включено' : 'Выключено'} </button> );}
Это просто и удобно, все необходимые пропсы собраны в одном месте. Но что будет, если мы захотим добавить свой onClick? Он будет перезаписан. Чтобы решить эту проблему, прибегаем к функции геттер пропсов.
Предложение, от которого невозможно отказаться

Далее рассмотрим подходы и паттерны, актуальные на сегодняшний день.
Prop Getter — функция, которая принимает пользовательские пропсы и объединяет их со стандартными, корректно вызывая оба обработчика. Это позволяет кастомизировать поведение без потери встроенной логики. Давайте модифицируем наш хук useToggle:
const useToggle = () => { const [on, setOn] = useState(false); const toggle = () => setOn(prev => !prev); const getButtonProps = (userProps = {}) => { const { onClick, ...rest } = userProps; return { onClick: (event) => { // встроенная логика toggle(); // пользовательская логика if (onClick) onClick(event); }, role: 'button', className: 'toggle-button', 'aria-pressed': on, // остальные пользовательские пропсы ...rest }; }; return { on, getButtonProps };}
Теперь вместо коллекции хук возвращает геттер getButtonProps, за счёт чего мы получаем большую гибкость, где пользователь может переопределить все, что нужно, сохранив основную логику:
const ToggleButton = () => { const { on, getButtonProps } = useToggle(); return ( <button {...getButtonProps({ className: 'my-button', onClick: () => console.log('Доп. действие'), })} > {on ? 'Включено' : 'Выключено'} </button> );};
Использование функции-геттер пропсов может показаться сложнее в реализации, однако если есть необходимость в расширении и переопределении любых пропсов, то это отличный паттерн.
В своём курсе «Advanced React Patterns” Кент С. Доддс заявляет:
«Коллекции пропсов не рекомендуются, и вам следует использовать только паттерн геттеров»
Если вы разрабатываете библиотеку или даже просто сложный кастомный хук внутри проекта, Prop Getters повысит его качество и удобство для других разработчиков (и для вас самих в будущем).
5. Управляемые пропсы (Control Props)
Популяризатор: Кент С. Доддс, 2018 год.
Суть: компонент либо управляет своим состоянием сам (неконтролируемый режим), либо делегирует управление родителю через пропсы (контролируемый режим).
Этот паттерн тесно связан с концепцией управляемых компонентов. Проще говоря, компонент либо живет своей жизнью, делая что-то по умолчанию или, если нужно, мы можем с помощью пропсов перехватить управление и он станет делать то, что скажет родительский компонент.
Рассмотрим паттерн на примере кнопки-переключателя. ToggleButton сам управляет своим состоянием включения и выключения, родитель не может его изменить:
const ToggleButton = () => { const [on, setOn] = useState(false); const toggle = () => setOn((prev) => !prev); return ( <button onClick={handleToggle}> {currentOn ? 'Включено' : 'Выключено'} </button> );};
Применим паттерн Control Props сделав так, чтобы компонент ToggleButton проверял, что, если передан on, то он выполняет его. Если нет, делает что-то по умолчанию:
const ToggleButton = ({ on, onChange }) => { // Внутреннее состояние для неконтролируемого режима const [innerOn, setInnerOn] = useState(false); // Определяем, контролируется ли компонент извне const isControlled = on !== undefined; const currentOn = isControlled ? on : innerOn; const handleToggle = () => { const nextOn = !currentOn; if (isControlled) { // Если контролируемый – уведомляем родителя onChange?.(nextOn); } else { // Иначе обновляем внутреннее состояние setInnerOn(nextOn); } }; return ( <button onClick={handleToggle}> {currentOn ? 'Включено' : 'Выключено'} </button> );};
На сегодняшний день паттерн используется, предоставляя гибкость и предсказуемость. Он вряд ли устареет в ближайшие годы, так как разработчики все чаще комбинируют Control Props с хуками-адаптерами, чтобы уменьшить бойлерплейт и сделать интерфейс более понятным и интуитивным.
Часто его применение можно встретить при реализации разнообразных UI-китов. Он дает разработчикам полный контроль над состоянием, сохраняя при этом удобство неконтролируемого использования «из коробки».
6. Слоты (Slot Pattern, Component Injection)
Первоисточник: Cam West при создании библиотеки “react-slot-fill”, 2017 год.
Суть: передаем отрисовку части компонентов родителю, получая их через пропсы.
В самом React нет встроенного синтаксиса для слотов, но паттерн легко эмулируется при помощи пропсов. Просто передаем готовые куски интерфейса в заранее определенные места.
Слоты чем-то похожи на ранее рассмотренный Render Props, но это разные уровни абстракции. Render Props более гибкий, но требует больше кода и может усложнять чтение. Слоты проще и легче читаются, но дают меньше контроля. Не получится завязаться на состоянии родителя, потому что родитель не передает вам данные, он просто вставляет ваш контент в нужное место.
Тем не менее, применяя этот подход, мы решаем проблему Props Drilling, ведь нам не нужно тащить пропсы через промежуточные слои, за счет чего увеличивается переиспользование макетов:
// 1. Создаем компонент в который будем вставлять “слоты”const Layout = ({ header, footer, children }) => { return ( <div> <header>{header}</header> <main>{children}</main> {/* дефолтный слот */} <footer>{footer}</footer> </div> );}// 2. Использование<Layout header={<h1>Заголовок</h1>} footer={<div>Футер</div>}> <p>Основной контент (children)</p></Layout>;
На сегодняшний день концепция слотов органично вписалась в современный React и стала стандартом для построения гибких интерфейсов. Их часто используют при разработке макетов (Layout-компоненты) и в UI-библиотеках (Radix UI, shadcn/ui), дающих разработчику полный контроль над разметкой.
Можно сказать, что слоты заняли место за главным столом современной React-разработки и не собираются его уступать.

7. Составные компоненты (Compound Components)
Первоисточник: Райан Флоренс, популяризатор: Кент С. Доддс
Суть: группа компонентов, которые разделяют общее состояние через контекст.
Родительский компонент управляет состоянием, а дочерние компоненты реагируют на него и отрисовывают себя соответствующим образом. При этом структура и порядок дочерних элементов задаются разработчиком, что дает полный контроль над разметкой.
Рассмотрим классический пример — компонент карточки. Создадим контейнер Card, и его части: Card.Header и Card.Body.
const Card = ({ children }) => { return <article>{children}</article>;};const CardHeader = ({ children }) => { return <header>{children}</header>;};const CardBody = ({ children }) => { return <section>{children}</section>;};// Сборка API (опционально)// Привязка через Card.Header = CardHeader даёт удобный API: Card.Header вместо отдельного импортаCard.Header = CardHeader;Card.Body = CardBody;
Использование:
<Card> <Card.Header>Заголовок</Card.Header> <Card.Body>Контент карточки</Card.Body></Card>
Получаем понятный и гибкий блок с единым визуалом. Разметка читается сверху вниз как в HTML. Можно менять порядок частей, пропускать ненужные или вкладывать свой контент.
Сегодня это один из базовых инструментов фронтенд-разработчика. Он востребован в дизайн-системах и UI-kit’ах. Библиотеки вроде Radix UI, shadcn и многие внутренние компонентные наборы строятся именно так: Dialog.Trigger, Tabs.List, Select.Item, Accordion.Content и т.д..
Когда гаснет неон
Ну что ж, наше путешествие по темным переулкам Лост-Хэвена подходит к концу. Мы проехали по всему городу, и пора подвести черту.
HOC, контейнеры и презентационные компоненты были разными ответами на один и тот же вопрос: как разделить логику состояния между компонентами, не переписывая её каждый раз? В какой-то мере хуки дали нам ответ, для переиспользования логики они оказались проще и прозрачнее, чем обертки и «умные» контейнеры.
«Ничего личного, Сонни. Это просто бизнес»
При этом хуки не заместили все паттерны. Compound Components, Slots, Render Props, Control Props и Prop Getters по-прежнему нужны там, где важны структура UI и контракт компонента. Хуки забрали себе другое: состояние и бизнес-логику. Если вы чувствуете, что компонент становится запутанным и трудночитаемым, найдите связанные строки кода и вынесите их в отдельные хуки. Это сделает код чище и понятнее.
Старые паттерны не исчезли бесследно, некоторые из них переродились, а некоторые уступили место новому кодексу. И он пишется функциями, которые начинаются с use.
ссылка на оригинал статьи https://habr.com/ru/articles/1055308/