Паттерны спрашивают, чтобы понять: умеешь ли ты проектировать компонент, который удобно переиспользовать, а не только «чтобы работало».
Композиция через children и слоты
Главный паттерн React: компонент не знает, что внутри, он даёт место.
function Card({ title, actions, children }) {
return (
<section className="card">
<header>{title}{actions}</header>
<div>{children}</div>
</section>
);
}
<Card title="Заказ" actions={<Button>Удалить</Button>}>
<OrderDetails />
</Card>
children и пропсы-элементы (слоты) убирают десятки флагов вида showDeleteButton, deleteButtonText. Бонус: элементы, переданные через children, создаёт родитель, поэтому при смене состояния внутри Card они не перерисуются заново.
Кастомные хуки
Способ переиспользовать логику с состоянием, а не разметку. Хук это функция, имя начинается с use, внутри можно вызывать другие хуки.
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn((v) => !v), []);
return [on, toggle];
}
Важно: два компонента, вызвавшие useToggle, получают независимые состояния. Хук делится логикой, а не данными. Общие данные дают Context или стор.
Кастомные хуки почти полностью вытеснили HOC и render props.
Render props
Компонент получает функцию и вызывает её, отдавая данные:
<DataLoader url="/api/users" render={(users) => <UserList users={users} />} />
Сейчас встречается в библиотеках (например, виртуальные списки, где строку рендерит ваша функция). Для своей логики обычно лучше хук.
HOC (компонент высшего порядка)
Функция, которая принимает компонент и возвращает новый: withAuth(Page). Минусы: непрозрачно, откуда пришёл пропс; конфликты имён пропсов; «ад обёрток» в DevTools. Нельзя создавать HOC внутри рендера: каждый раз получится новый тип компонента, и React размонтирует поддерево со всем состоянием.
Compound components
Набор компонентов, которые работают вместе и делят состояние через Context:
<Tabs defaultValue="a">
<Tabs.List>
<Tabs.Tab value="a">Профиль</Tabs.Tab>
<Tabs.Tab value="b">Настройки</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="a">...</Tabs.Panel>
<Tabs.Panel value="b">...</Tabs.Panel>
</Tabs>
Внутри Tabs хранит активную вкладку и отдаёт её детям через контекст. Пользователь сам решает порядок и разметку. Так устроены Radix, Headless UI, MUI Tabs.
Контролируемый и неконтролируемый компонент
Хороший переиспользуемый компонент умеет оба режима, как <input>:
function Tabs({ value, defaultValue, onValueChange, children }) {
const [inner, setInner] = useState(defaultValue);
const isControlled = value !== undefined;
const active = isControlled ? value : inner;
const select = (v) => {
if (!isControlled) setInner(v);
onValueChange?.(v);
};
// ...
}
Если передан value, правду хранит родитель; если нет, компонент хранит сам.
Headless-компоненты
Логика и доступность без стилей: хук или компонент отдаёт состояние и пропсы для элементов, разметку и стили пишете вы. Пример идеи: useCombobox возвращает getInputProps() и getItemProps().
Error boundary
Ловит ошибки рендера в поддереве и показывает запасной интерфейс. До сих пор пишется только классом (static getDerivedStateFromError, componentDidCatch), хука для этого в React нет. На практике берут пакет react-error-boundary. Не ловит ошибки в обработчиках событий и в асинхронном коде вне рендера.
Мелочи React 19, которые влияют на паттерны
refтеперь обычный проп у функциональных компонентов:function Input({ ref, ...props }).forwardRefбольше не нужен.- Context рендерится как провайдер:
<TabsContext value={...}>. use(Context)можно вызывать в условии, в отличие отuseContext.
Частые ошибки
- Компонент с 20 булевыми пропсами вместо слотов.
- Ожидание, что кастомный хук делит состояние между компонентами.
- Определение компонента внутри другого компонента: на каждом рендере новый тип, потеря состояния и фокуса.
- Смешение контролируемого и неконтролируемого режима: компонент меняет режим посреди жизни.
Что спросят на собесе
- Чем кастомный хук лучше HOC и render props?
- Как бы ты спроектировал компонент Tabs или Select для дизайн-системы?
- Что такое compound components и как они делят состояние?
- Почему нельзя объявлять компонент внутри другого компонента?
- Как поймать ошибку рендера и чего error boundary не ловит?