Посмотрите на нативный HTML:
<select>
<option value="ru">Русский</option>
<option value="en">English</option>
<option value="de">Deutsch</option>
</select><select> и <option> — это составные компоненты. Они работают вместе: <select> управляет состоянием (какой вариант выбран), а <option> знает о своём контексте без явной передачи пропсов.
Compound Components в React — это паттерн, где несколько компонентов неявно делят состояние через Context или через React.Children + cloneElement:
// Использование похоже на нативный select/option:
<Tabs defaultValue="profile">
<Tab value="profile">Профиль</Tab>
<Tab value="settings">Настройки</Tab>
<Tab value="security">Безопасность</Tab>
<TabPanel value="profile">
<ProfileContent />
</TabPanel>
<TabPanel value="settings">
<SettingsContent />
</TabPanel>
</Tabs>Компоненты Tab и TabPanel не получают activeTab явно — они берут его из Context.
// 1. Создаём контекст для обмена состоянием
const TabsContext = createContext(null)
// 2. Корневой компонент управляет состоянием
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
// 3. Дочерние компоненты читают контекст
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext)
const isActive = activeTab === value
return (
<button
className={isActive ? 'tab active' : 'tab'}
onClick={() => setActiveTab(value)}
>
{children}
</button>
)
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext)
if (activeTab !== value) return null
return <div className="tab-panel">{children}</div>
}
// Добавляем как статические свойства для удобного импорта:
Tabs.Tab = Tab
Tabs.Panel = TabPanel// Удобный синтаксис — всё из одного импорта:
import { Tabs } from './Tabs'
<Tabs defaultValue="home">
<Tabs.Tab value="home">Главная</Tabs.Tab>
<Tabs.Tab value="about">О нас</Tabs.Tab>
<Tabs.Panel value="home"><HomePage /></Tabs.Panel>
<Tabs.Panel value="about"><AboutPage /></Tabs.Panel>
</Tabs>Более старый подход — без Context, через явное клонирование:
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue)
// Клонируем каждый дочерний элемент и добавляем пропсы
const enhancedChildren = React.Children.map(children, child => {
if (child.type === Tab) {
return React.cloneElement(child, {
isActive: activeTab === child.props.value,
onSelect: setActiveTab,
})
}
return child
})
return <div>{enhancedChildren}</div>
}Подход через Context предпочтительнее: он работает с любой глубиной вложенности, а cloneElement — только с прямыми детьми.
Radix UI (Headless UI):
<Dialog.Root>
<Dialog.Trigger>Открыть</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Заголовок</Dialog.Title>
<Dialog.Description>Описание</Dialog.Description>
<Dialog.Close>Закрыть</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>React Router:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>Паттерн уместен когда:
Не нужен когда компоненты просто принимают данные через пропсы — не усложняйте без необходимости.
Реализация паттерна Compound Components на JavaScript: система вкладок со shared state через замыкание, статические свойства и контекст
// Демонстрируем Compound Components через замыкания (аналог Context в React).
// Каждый "компонент" — функция, получающая доступ к shared state через closure.
// --- Создаём систему вкладок ---
function createTabSystem(defaultTab) {
// Shared state — аналог Context
let activeTab = defaultTab
const tabs = [] // зарегистрированные вкладки
const panels = [] // зарегистрированные панели
const listeners = [] // подписчики на изменение
function notify() {
listeners.forEach(fn => fn(activeTab))
}
// --- Корневой компонент ---
function Tabs(config) {
return {
type: 'Tabs',
activeTab,
children: config.children || [],
render() {
const tabBar = tabs.map(tab => tab.render()).join(' | ')
const activePanel = panels.find(p => p.value === activeTab)
const content = activePanel ? activePanel.render() : '[Панель не найдена]'
return '[ Вкладки: ' + tabBar + ' ]
[ Содержимое: ' + content + ' ]'
}
}
}
// --- Дочерний компонент Tab ---
// Имеет доступ к shared state через замыкание
function Tab(config) {
const { value, label } = config
tabs.push({
value,
render() {
const isActive = activeTab === value
return isActive ? '【' + label + '】' : label
}
})
return {
type: 'Tab',
value,
label,
activate() {
activeTab = value
notify()
console.log('Переключено на вкладку:', value)
}
}
}
// --- Дочерний компонент TabPanel ---
function TabPanel(config) {
const { value, content } = config
panels.push({
value,
render() { return content }
})
return {
type: 'TabPanel',
value,
isVisible: () => activeTab === value
}
}
// --- Составной интерфейс (статические свойства) ---
const TabSystem = {
Tabs,
Tab,
TabPanel,
onChange: (fn) => listeners.push(fn),
render() {
const tabBar = tabs.map(t => t.render()).join(' | ')
const activePanel = panels.find(p => p.value === activeTab)
const content = activePanel ? activePanel.render() : '—'
return '┌─ Tabs ─────────────────────┐
' +
'│ ' + tabBar + '
' +
'├────────────────────────────┤
' +
'│ ' + content + '
' +
'└────────────────────────────┘'
}
}
return TabSystem
}
// --- Использование ---
console.log('=== Compound Components: Tabs ===')
const TabSystem = createTabSystem('profile')
// "Рендерим" дочерние компоненты — они регистрируют себя в shared state
const profileTab = TabSystem.Tab({ value: 'profile', label: 'Профиль' })
const settingsTab = TabSystem.Tab({ value: 'settings', label: 'Настройки' })
const securityTab = TabSystem.Tab({ value: 'security', label: 'Безопасность' })
TabSystem.TabPanel({ value: 'profile', content: 'Здесь данные профиля: имя, фото, bio' })
TabSystem.TabPanel({ value: 'settings', content: 'Здесь настройки: язык, тема, уведомления' })
TabSystem.TabPanel({ value: 'security', content: 'Здесь безопасность: пароль, 2FA, сессии' })
TabSystem.onChange(tab => console.log('Событие onChange: активная вкладка =', tab))
// Начальное состояние
console.log('
Начальный рендер (profile активна):')
console.log(TabSystem.render())
// Переключение вкладки
settingsTab.activate()
console.log('
После переключения на settings:')
console.log(TabSystem.render())
securityTab.activate()
console.log('
После переключения на security:')
console.log(TabSystem.render())
// --- Accordion: ещё один пример Compound Components ---
function createAccordion(allowMultiple = false) {
const items = new Map()
let openItems = new Set()
const Accordion = {
Item(id, header, content) {
items.set(id, { id, header, content })
return {
type: 'Accordion.Item',
id,
toggle() {
if (openItems.has(id)) {
openItems.delete(id)
} else {
if (!allowMultiple) openItems.clear()
openItems.add(id)
}
},
isOpen: () => openItems.has(id)
}
},
render() {
let output = '=== Accordion ===
'
items.forEach(({ id, header, content }) => {
const isOpen = openItems.has(id)
output += isOpen
? '▼ ' + header + '
' + content + '
'
: '▶ ' + header + '
'
})
return output
}
}
return Accordion
}
console.log('
=== Compound Components: Accordion ===')
const accordion = createAccordion(false) // только один открытый
const item1 = accordion.Item('q1', 'Что такое React?', 'React — библиотека для UI')
const item2 = accordion.Item('q2', 'Что такое Virtual DOM?', 'Виртуальное дерево DOM для быстрого diff')
const item3 = accordion.Item('q3', 'Что такое хуки?', 'Функции для работы со состоянием в функциональных компонентах')
console.log(accordion.render())
item1.toggle()
console.log('После открытия q1:')
console.log(accordion.render())
item2.toggle()
console.log('После открытия q2 (q1 должен закрыться):')
console.log(accordion.render())Посмотрите на нативный HTML:
<select>
<option value="ru">Русский</option>
<option value="en">English</option>
<option value="de">Deutsch</option>
</select><select> и <option> — это составные компоненты. Они работают вместе: <select> управляет состоянием (какой вариант выбран), а <option> знает о своём контексте без явной передачи пропсов.
Compound Components в React — это паттерн, где несколько компонентов неявно делят состояние через Context или через React.Children + cloneElement:
// Использование похоже на нативный select/option:
<Tabs defaultValue="profile">
<Tab value="profile">Профиль</Tab>
<Tab value="settings">Настройки</Tab>
<Tab value="security">Безопасность</Tab>
<TabPanel value="profile">
<ProfileContent />
</TabPanel>
<TabPanel value="settings">
<SettingsContent />
</TabPanel>
</Tabs>Компоненты Tab и TabPanel не получают activeTab явно — они берут его из Context.
// 1. Создаём контекст для обмена состоянием
const TabsContext = createContext(null)
// 2. Корневой компонент управляет состоянием
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
<div className="tabs">{children}</div>
</TabsContext.Provider>
)
}
// 3. Дочерние компоненты читают контекст
function Tab({ value, children }) {
const { activeTab, setActiveTab } = useContext(TabsContext)
const isActive = activeTab === value
return (
<button
className={isActive ? 'tab active' : 'tab'}
onClick={() => setActiveTab(value)}
>
{children}
</button>
)
}
function TabPanel({ value, children }) {
const { activeTab } = useContext(TabsContext)
if (activeTab !== value) return null
return <div className="tab-panel">{children}</div>
}
// Добавляем как статические свойства для удобного импорта:
Tabs.Tab = Tab
Tabs.Panel = TabPanel// Удобный синтаксис — всё из одного импорта:
import { Tabs } from './Tabs'
<Tabs defaultValue="home">
<Tabs.Tab value="home">Главная</Tabs.Tab>
<Tabs.Tab value="about">О нас</Tabs.Tab>
<Tabs.Panel value="home"><HomePage /></Tabs.Panel>
<Tabs.Panel value="about"><AboutPage /></Tabs.Panel>
</Tabs>Более старый подход — без Context, через явное клонирование:
function Tabs({ children, defaultValue }) {
const [activeTab, setActiveTab] = useState(defaultValue)
// Клонируем каждый дочерний элемент и добавляем пропсы
const enhancedChildren = React.Children.map(children, child => {
if (child.type === Tab) {
return React.cloneElement(child, {
isActive: activeTab === child.props.value,
onSelect: setActiveTab,
})
}
return child
})
return <div>{enhancedChildren}</div>
}Подход через Context предпочтительнее: он работает с любой глубиной вложенности, а cloneElement — только с прямыми детьми.
Radix UI (Headless UI):
<Dialog.Root>
<Dialog.Trigger>Открыть</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content>
<Dialog.Title>Заголовок</Dialog.Title>
<Dialog.Description>Описание</Dialog.Description>
<Dialog.Close>Закрыть</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>React Router:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>Паттерн уместен когда:
Не нужен когда компоненты просто принимают данные через пропсы — не усложняйте без необходимости.
Реализация паттерна Compound Components на JavaScript: система вкладок со shared state через замыкание, статические свойства и контекст
// Демонстрируем Compound Components через замыкания (аналог Context в React).
// Каждый "компонент" — функция, получающая доступ к shared state через closure.
// --- Создаём систему вкладок ---
function createTabSystem(defaultTab) {
// Shared state — аналог Context
let activeTab = defaultTab
const tabs = [] // зарегистрированные вкладки
const panels = [] // зарегистрированные панели
const listeners = [] // подписчики на изменение
function notify() {
listeners.forEach(fn => fn(activeTab))
}
// --- Корневой компонент ---
function Tabs(config) {
return {
type: 'Tabs',
activeTab,
children: config.children || [],
render() {
const tabBar = tabs.map(tab => tab.render()).join(' | ')
const activePanel = panels.find(p => p.value === activeTab)
const content = activePanel ? activePanel.render() : '[Панель не найдена]'
return '[ Вкладки: ' + tabBar + ' ]
[ Содержимое: ' + content + ' ]'
}
}
}
// --- Дочерний компонент Tab ---
// Имеет доступ к shared state через замыкание
function Tab(config) {
const { value, label } = config
tabs.push({
value,
render() {
const isActive = activeTab === value
return isActive ? '【' + label + '】' : label
}
})
return {
type: 'Tab',
value,
label,
activate() {
activeTab = value
notify()
console.log('Переключено на вкладку:', value)
}
}
}
// --- Дочерний компонент TabPanel ---
function TabPanel(config) {
const { value, content } = config
panels.push({
value,
render() { return content }
})
return {
type: 'TabPanel',
value,
isVisible: () => activeTab === value
}
}
// --- Составной интерфейс (статические свойства) ---
const TabSystem = {
Tabs,
Tab,
TabPanel,
onChange: (fn) => listeners.push(fn),
render() {
const tabBar = tabs.map(t => t.render()).join(' | ')
const activePanel = panels.find(p => p.value === activeTab)
const content = activePanel ? activePanel.render() : '—'
return '┌─ Tabs ─────────────────────┐
' +
'│ ' + tabBar + '
' +
'├────────────────────────────┤
' +
'│ ' + content + '
' +
'└────────────────────────────┘'
}
}
return TabSystem
}
// --- Использование ---
console.log('=== Compound Components: Tabs ===')
const TabSystem = createTabSystem('profile')
// "Рендерим" дочерние компоненты — они регистрируют себя в shared state
const profileTab = TabSystem.Tab({ value: 'profile', label: 'Профиль' })
const settingsTab = TabSystem.Tab({ value: 'settings', label: 'Настройки' })
const securityTab = TabSystem.Tab({ value: 'security', label: 'Безопасность' })
TabSystem.TabPanel({ value: 'profile', content: 'Здесь данные профиля: имя, фото, bio' })
TabSystem.TabPanel({ value: 'settings', content: 'Здесь настройки: язык, тема, уведомления' })
TabSystem.TabPanel({ value: 'security', content: 'Здесь безопасность: пароль, 2FA, сессии' })
TabSystem.onChange(tab => console.log('Событие onChange: активная вкладка =', tab))
// Начальное состояние
console.log('
Начальный рендер (profile активна):')
console.log(TabSystem.render())
// Переключение вкладки
settingsTab.activate()
console.log('
После переключения на settings:')
console.log(TabSystem.render())
securityTab.activate()
console.log('
После переключения на security:')
console.log(TabSystem.render())
// --- Accordion: ещё один пример Compound Components ---
function createAccordion(allowMultiple = false) {
const items = new Map()
let openItems = new Set()
const Accordion = {
Item(id, header, content) {
items.set(id, { id, header, content })
return {
type: 'Accordion.Item',
id,
toggle() {
if (openItems.has(id)) {
openItems.delete(id)
} else {
if (!allowMultiple) openItems.clear()
openItems.add(id)
}
},
isOpen: () => openItems.has(id)
}
},
render() {
let output = '=== Accordion ===
'
items.forEach(({ id, header, content }) => {
const isOpen = openItems.has(id)
output += isOpen
? '▼ ' + header + '
' + content + '
'
: '▶ ' + header + '
'
})
return output
}
}
return Accordion
}
console.log('
=== Compound Components: Accordion ===')
const accordion = createAccordion(false) // только один открытый
const item1 = accordion.Item('q1', 'Что такое React?', 'React — библиотека для UI')
const item2 = accordion.Item('q2', 'Что такое Virtual DOM?', 'Виртуальное дерево DOM для быстрого diff')
const item3 = accordion.Item('q3', 'Что такое хуки?', 'Функции для работы со состоянием в функциональных компонентах')
console.log(accordion.render())
item1.toggle()
console.log('После открытия q1:')
console.log(accordion.render())
item2.toggle()
console.log('После открытия q2 (q1 должен закрыться):')
console.log(accordion.render())Реализуй систему Tabs с паттерном Compound Components. Создай Context для обмена состоянием между компонентами. Tabs управляет активной вкладкой, Tab — кнопка переключения, TabPanel — содержимое вкладки. Дочерние компоненты получают состояние через useContext.
В Tabs: useState(defaultValue), Provider value={{ activeTab, setActiveTab }}. В Tab и TabPanel: useContext(TabsContext). В TabPanel проверка: if (activeTab !== value) return null