react-composition-2026
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseModern React Composition Patterns
现代React组合模式
Composition patterns for building flexible, maintainable React components that scale. These patterns replace boolean-prop proliferation, rigid component APIs, and tangled state with composable, explicit designs.
用于构建可扩展、灵活且易于维护的React组件的组合模式。这些模式通过可组合的明确设计,替代了大量布尔型props、僵化的组件API以及混乱的状态管理方式。
When to Use
适用场景
Reference these patterns when:
- A component has more than 3-4 boolean props controlling its behavior
- Building reusable UI components or a shared component library
- Refactoring components that are difficult to extend
- Designing component APIs that other teams will consume
- Reviewing component architecture for flexibility and maintainability
在以下场景中可以参考这些模式:
- 某个组件拥有3-4个以上控制其行为的布尔型props时
- 构建可复用UI组件或共享组件库时
- 重构难以扩展的组件时
- 设计供其他团队使用的组件API时
- 评审组件架构的灵活性与可维护性时
Instructions
使用说明
- Apply these patterns during component design, code generation, and review. When you see boolean prop accumulation or rigid component APIs, suggest the appropriate composition pattern.
- 在组件设计、代码生成和评审阶段应用这些模式。当你发现组件积累了大量布尔型props或API僵化时,建议采用合适的组合模式。
Details
详细内容
Overview
概述
The core principle: composition over configuration. Instead of adding boolean props and conditional branches to handle every variant, compose smaller, focused components together. This makes components easier to understand, test, and extend — for both humans and AI agents.
核心原则:组合优于配置。无需通过添加布尔型props和条件分支来处理所有变体,而是将更小、职责单一的组件组合在一起。这让组件对人类和AI Agent来说都更易于理解、测试和扩展。
1. Replace Boolean Props with Composition
1. 用组合替代布尔型Props
Impact: HIGH — Prevents combinatorial explosion and makes intent explicit.
Boolean props multiply complexity: 4 booleans = 16 possible states, most of which are untested. Replace them with composable children.
Avoid — boolean prop accumulation:
tsx
<Card
showHeader
showFooter
collapsible
bordered
withShadow
headerAction="close"
size="large"
/>Prefer — explicit composition:
tsx
<Card variant="bordered" shadow="md">
<Card.Header>
<h3>Title</h3>
<Card.CloseButton />
</Card.Header>
<Card.Body collapsible>
<p>Content here</p>
</Card.Body>
<Card.Footer>
<Button>Save</Button>
</Card.Footer>
</Card>Each piece is explicit, testable, and independently optional.
影响程度:高 —— 避免组合爆炸,让意图更明确。
布尔型props会成倍增加复杂度:4个布尔值就对应16种可能的状态,其中大多数都未经过测试。用可组合的子组件替代它们。
避免——积累大量布尔型props:
tsx
<Card
showHeader
showFooter
collapsible
bordered
withShadow
headerAction="close"
size="large"
/>推荐——明确的组合方式:
tsx
<Card variant="bordered" shadow="md">
<Card.Header>
<h3>Title</h3>
<Card.CloseButton />
</Card.Header>
<Card.Body collapsible>
<p>Content here</p>
</Card.Body>
<Card.Footer>
<Button>Save</Button>
</Card.Footer>
</Card>每个部分都清晰明确、可独立测试且为可选内容。
2. Build Compound Components with Context
2. 结合Context构建复合组件
Impact: HIGH — Shared implicit state without prop drilling.
Compound components are a group of components that work together, sharing state through context rather than props. The parent owns the state; children consume it.
Avoid — parent manages everything through props:
tsx
<Select
options={options}
value={value}
onChange={onChange}
renderOption={(opt) => <span>{opt.icon} {opt.label}</span>}
renderSelected={(opt) => <b>{opt.label}</b>}
placeholder="Choose..."
clearable
searchable
maxHeight={300}
/>Prefer — compound components:
tsx
const SelectContext = createContext<SelectState | null>(null)
function Select({ children, value, onChange }: SelectProps) {
const [open, setOpen] = useState(false)
const ctx = useMemo(() => ({ value, onChange, open, setOpen }), [value, onChange, open])
return (
<SelectContext.Provider value={ctx}>
<div className="select-root">{children}</div>
</SelectContext.Provider>
)
}
function Trigger({ children }: { children: React.ReactNode }) {
const { open, setOpen } = useSelectContext()
return <button onClick={() => setOpen(!open)}>{children}</button>
}
function Options({ children }: { children: React.ReactNode }) {
const { open } = useSelectContext()
if (!open) return null
return <ul role="listbox">{children}</ul>
}
function Option({ value, children }: OptionProps) {
const { value: selected, onChange, setOpen } = useSelectContext()
return (
<li
role="option"
aria-selected={value === selected}
onClick={() => { onChange(value); setOpen(false) }}
>
{children}
</li>
)
}
Select.Trigger = Trigger
Select.Options = Options
Select.Option = OptionUsage:
tsx
<Select value={color} onChange={setColor}>
<Select.Trigger>Pick a color</Select.Trigger>
<Select.Options>
<Select.Option value="red">Red</Select.Option>
<Select.Option value="blue">Blue</Select.Option>
</Select.Options>
</Select>影响程度:高 —— 无需prop透传即可共享隐式状态。
复合组件是一组协同工作的组件,它们通过Context而非props共享状态。父组件拥有状态,子组件消费状态。
避免——父组件通过props管理所有内容:
tsx
<Select
options={options}
value={value}
onChange={onChange}
renderOption={(opt) => <span>{opt.icon} {opt.label}</span>}
renderSelected={(opt) => <b>{opt.label}</b>}
placeholder="Choose..."
clearable
searchable
maxHeight={300}
/>推荐——复合组件:
tsx
const SelectContext = createContext<SelectState | null>(null)
function Select({ children, value, onChange }: SelectProps) {
const [open, setOpen] = useState(false)
const ctx = useMemo(() => ({ value, onChange, open, setOpen }), [value, onChange, open])
return (
<SelectContext.Provider value={ctx}>
<div className="select-root">{children}</div>
</SelectContext.Provider>
)
}
function Trigger({ children }: { children: React.ReactNode }) {
const { open, setOpen } = useSelectContext()
return <button onClick={() => setOpen(!open)}>{children}</button>
}
function Options({ children }: { children: React.ReactNode }) {
const { open } = useSelectContext()
if (!open) return null
return <ul role="listbox">{children}</ul>
}
function Option({ value, children }: OptionProps) {
const { value: selected, onChange, setOpen } = useSelectContext()
return (
<li
role="option"
aria-selected={value === selected}
onClick={() => { onChange(value); setOpen(false) }}
>
{children}
</li>
)
}
Select.Trigger = Trigger
Select.Options = Options
Select.Option = Option使用示例:
tsx
<Select value={color} onChange={setColor}>
<Select.Trigger>Pick a color</Select.Trigger>
<Select.Options>
<Select.Option value="red">Red</Select.Option>
<Select.Option value="blue">Blue</Select.Option>
</Select.Options>
</Select>3. Create Explicit Variant Components
3. 创建明确的变体组件
Impact: MEDIUM — Makes each mode a clear, focused component.
When a component has distinct "modes" (dialog vs drawer, inline vs modal, card vs list-item), create explicit variant components instead of toggling with props.
Avoid — one component with mode props:
tsx
function MediaDisplay({ type, src, title, showControls, autoPlay, loop }: Props) {
if (type === 'video') {
return <video src={src} controls={showControls} autoPlay={autoPlay} loop={loop} />
}
if (type === 'audio') {
return <audio src={src} controls={showControls} />
}
return <img src={src} alt={title} />
}Prefer — explicit variants:
tsx
function VideoPlayer({ src, controls, autoPlay, loop }: VideoProps) {
return <video src={src} controls={controls} autoPlay={autoPlay} loop={loop} />
}
function AudioPlayer({ src, controls }: AudioProps) {
return <audio src={src} controls={controls} />
}
function Image({ src, alt }: ImageProps) {
return <img src={src} alt={alt} />
}Each variant has exactly the props it needs — no impossible states, no unused props.
影响程度:中 —— 让每种模式都成为清晰、职责单一的组件。
当一个组件有多种不同“模式”(如对话框与抽屉、内联与模态框、卡片与列表项)时,创建明确的变体组件,而非通过props切换模式。
避免——单个组件通过props切换模式:
tsx
function MediaDisplay({ type, src, title, showControls, autoPlay, loop }: Props) {
if (type === 'video') {
return <video src={src} controls={showControls} autoPlay={autoPlay} loop={loop} />
}
if (type === 'audio') {
return <audio src={src} controls={showControls} />
}
return <img src={src} alt={title} />
}推荐——明确的变体组件:
tsx
function VideoPlayer({ src, controls, autoPlay, loop }: VideoProps) {
return <video src={src} controls={controls} autoPlay={autoPlay} loop={loop} />
}
function AudioPlayer({ src, controls }: AudioProps) {
return <audio src={src} controls={controls} />
}
function Image({ src, alt }: ImageProps) {
return <img src={src} alt={alt} />
}每个变体组件只包含它需要的props——不存在不可能的状态,也没有未使用的props。
4. Use Children Over Render Props for Composition
4. 用Children而非Render Props实现组合
Impact: MEDIUM — Simpler API, better readability.
Render props (, ) were essential before hooks, but today provides cleaner composition for most cases.
renderHeaderrenderItemchildrenAvoid — render prop proliferation:
tsx
<DataTable
data={users}
renderHeader={() => <h2>Users</h2>}
renderRow={(user) => <UserRow user={user} />}
renderEmpty={() => <EmptyState />}
renderFooter={() => <Pagination />}
/>Prefer — children composition:
tsx
<DataTable data={users}>
<DataTable.Header>
<h2>Users</h2>
</DataTable.Header>
<DataTable.Body>
{users.map(user => <UserRow key={user.id} user={user} />)}
</DataTable.Body>
<DataTable.Empty>
<EmptyState />
</DataTable.Empty>
<DataTable.Footer>
<Pagination />
</DataTable.Footer>
</DataTable>Reserve render props for cases where the parent needs to provide data to the renderer (e.g., virtualized list items).
影响程度:中 —— API更简洁,可读性更强。
在Hooks出现之前,Render Props(如、)是必不可少的,但如今对于大多数场景,能提供更简洁的组合方式。
renderHeaderrenderItemchildren避免——大量使用Render Props:
tsx
<DataTable
data={users}
renderHeader={() => <h2>Users</h2>}
renderRow={(user) => <UserRow user={user} />}
renderEmpty={() => <EmptyState />}
renderFooter={() => <Pagination />}
/>推荐——用Children实现组合:
tsx
<DataTable data={users}>
<DataTable.Header>
<h2>Users</h2>
</DataTable.Header>
<DataTable.Body>
{users.map(user => <UserRow key={user.id} user={user} />)}
</DataTable.Body>
<DataTable.Empty>
<EmptyState />
</DataTable.Empty>
<DataTable.Footer>
<Pagination />
</DataTable.Footer>
</DataTable>仅在父组件需要向渲染器提供数据的场景中保留Render Props(如虚拟化列表项)。
5. Decouple State Implementation from UI
5. 将状态实现与UI解耦
Impact: MEDIUM — Swap state management without changing components.
Define a generic interface for your state shape (value, actions, metadata), then let providers implement it. Components consume the interface, not the implementation.
Define the interface:
typescript
interface CounterState {
count: number
increment: () => void
decrement: () => void
isLoading: boolean
}
const CounterContext = createContext<CounterState | null>(null)
function useCounter() {
const ctx = useContext(CounterContext)
if (!ctx) throw new Error('useCounter must be used within a CounterProvider')
return ctx
}Implement with local state:
tsx
function LocalCounterProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0)
const value = useMemo(() => ({
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
isLoading: false,
}), [count])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}Swap to API-backed state without changing consumers:
tsx
function ApiCounterProvider({ children }: { children: React.ReactNode }) {
const { data, mutate } = useSWR('/api/counter', fetcher)
const value = useMemo(() => ({
count: data?.count ?? 0,
increment: () => mutate(patch('/api/counter', { delta: 1 })),
decrement: () => mutate(patch('/api/counter', { delta: -1 })),
isLoading: !data,
}), [data, mutate])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}The consumers never change.
useCounter()影响程度:中 —— 无需修改组件即可切换状态管理方式。
为状态形状(值、操作、元数据)定义通用接口,然后由提供者实现该接口。组件消费接口而非具体实现。
定义接口:
typescript
interface CounterState {
count: number
increment: () => void
decrement: () => void
isLoading: boolean
}
const CounterContext = createContext<CounterState | null>(null)
function useCounter() {
const ctx = useContext(CounterContext)
if (!ctx) throw new Error('useCounter must be used within a CounterProvider')
return ctx
}用本地状态实现:
tsx
function LocalCounterProvider({ children }: { children: React.ReactNode }) {
const [count, setCount] = useState(0)
const value = useMemo(() => ({
count,
increment: () => setCount(c => c + 1),
decrement: () => setCount(c => c - 1),
isLoading: false,
}), [count])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}无需修改消费者即可切换为API驱动的状态:
tsx
function ApiCounterProvider({ children }: { children: React.ReactNode }) {
const { data, mutate } = useSWR('/api/counter', fetcher)
const value = useMemo(() => ({
count: data?.count ?? 0,
increment: () => mutate(patch('/api/counter', { delta: 1 })),
decrement: () => mutate(patch('/api/counter', { delta: -1 })),
isLoading: !data,
}), [data, mutate])
return <CounterContext.Provider value={value}>{children}</CounterContext.Provider>
}使用的消费者无需做任何修改。
useCounter()6. Lift State to Provider Components
6. 将状态提升至Provider组件
Impact: MEDIUM — Enables sibling communication without prop threading.
When two sibling components need shared state, lift it into a provider rather than threading callbacks through the parent.
Avoid — parent threads state to siblings:
tsx
function Page() {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</div>
)
}Prefer — provider manages shared state:
tsx
function SelectionProvider({ children }: { children: React.ReactNode }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<SelectionContext.Provider value={{ selected, setSelected }}>
{children}
</SelectionContext.Provider>
)
}
function Page() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
)
}Both and consume directly.
SidebarDetailuseSelection()影响程度:中 —— 无需prop透传即可实现兄弟组件通信。
当两个兄弟组件需要共享状态时,将状态提升至Provider中,而非通过父组件传递回调函数。
避免——父组件向兄弟组件传递状态:
tsx
function Page() {
const [selected, setSelected] = useState<string | null>(null)
return (
<div>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</div>
)
}推荐——由Provider管理共享状态:
tsx
function SelectionProvider({ children }: { children: React.ReactNode }) {
const [selected, setSelected] = useState<string | null>(null)
return (
<SelectionContext.Provider value={{ selected, setSelected }}>
{children}
</SelectionContext.Provider>
)
}
function Page() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
)
}SidebarDetailuseSelection()7. Use Polymorphic as
Props for Flexible Elements
as7. 用多态as
Props实现灵活元素
asImpact: MEDIUM — One component, any underlying element or component.
The prop pattern lets consumers control the rendered element while keeping your component's styles and behavior.
astsx
type BoxProps<C extends React.ElementType = 'div'> = {
as?: C
children: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<C>, 'as' | 'children'>
function Box<C extends React.ElementType = 'div'>({
as,
children,
...props
}: BoxProps<C>) {
const Component = as || 'div'
return <Component {...props}>{children}</Component>
}Usage:
tsx
<Box>Default div</Box>
<Box as="section">A section</Box>
<Box as="a" href="/about">A link</Box>
<Box as={Link} to="/about">Router link</Box>影响程度:中 —— 单个组件可对应任意底层元素或组件。
astsx
type BoxProps<C extends React.ElementType = 'div'> = {
as?: C
children: React.ReactNode
} & Omit<React.ComponentPropsWithoutRef<C>, 'as' | 'children'>
function Box<C extends React.ElementType = 'div'>({
as,
children,
...props
}: BoxProps<C>) {
const Component = as || 'div'
return <Component {...props}>{children}</Component>
}使用示例:
tsx
<Box>默认div</Box>
<Box as="section">一个section元素</Box>
<Box as="a" href="/about">一个链接</Box>
<Box as={Link} to="/about">路由链接</Box>8. React 19: Drop forwardRef
, Use ref
as a Prop
forwardRefref8. React 19:弃用forwardRef
,将ref
作为普通Props使用
forwardRefrefImpact: MEDIUM — Simpler component definitions.
React 19 passes as a regular prop. No more wrapper.
refforwardRefReact 18 (deprecated pattern):
tsx
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return <input ref={ref} {...props} />
})React 19:
tsx
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />
}Similarly, can read either promises or context and can be called conditionally:
use()tsx
import { use } from 'react'
function Panel({ themePromise }: { themePromise: Promise<Theme> }) {
const theme = use(themePromise) // unwraps promise
const user = use(UserContext) // conditional context read
return <div className={theme.bg}>{user.name}</div>
}影响程度:中 —— 组件定义更简洁。
在React 19中,作为普通Props传递,无需再使用包装。
refforwardRefReact 18(已弃用模式):
tsx
const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
return <input ref={ref} {...props} />
})React 19:
tsx
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />
}同样,可以读取Promise或Context,并且可以被条件调用:
use()tsx
import { use } from 'react'
function Panel({ themePromise }: { themePromise: Promise<Theme> }) {
const theme = use(themePromise) // 解析Promise
const user = use(UserContext) // 条件读取Context
return <div className={theme.bg}>{user.name}</div>
}9. Slot Pattern for Layout Components
9. 布局组件的Slot模式
Impact: MEDIUM — Named insertion points without render props.
For layout components with multiple content areas, use a slot pattern based on child type detection or named sub-components.
tsx
function AppLayout({ children }: { children: React.ReactNode }) {
const slots = React.Children.toArray(children)
const header = slots.find(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === AppLayout.Header
)
const content = slots.filter(
(child) => !React.isValidElement(child) || child.type !== AppLayout.Header
)
return (
<div className="app-layout">
<header>{header}</header>
<main>{content}</main>
</div>
)
}
AppLayout.Header = function Header({ children }: { children: React.ReactNode }) {
return <>{children}</>
}Usage:
tsx
<AppLayout>
<AppLayout.Header>
<Logo />
<Nav />
</AppLayout.Header>
<Dashboard />
</AppLayout>影响程度:中 —— 无需Render Props即可实现命名插入点。
对于拥有多个内容区域的布局组件,可基于子元素类型检测或命名子组件实现Slot模式。
tsx
function AppLayout({ children }: { children: React.ReactNode }) {
const slots = React.Children.toArray(children)
const header = slots.find(
(child): child is React.ReactElement => React.isValidElement(child) && child.type === AppLayout.Header
)
const content = slots.filter(
(child) => !React.isValidElement(child) || child.type !== AppLayout.Header
)
return (
<div className="app-layout">
<header>{header}</header>
<main>{content}</main>
</div>
)
}
AppLayout.Header = function Header({ children }: { children: React.ReactNode }) {
return <>{children}</>
}使用示例:
tsx
<AppLayout>
<AppLayout.Header>
<Logo />
<Nav />
</AppLayout.Header>
<Dashboard />
</AppLayout>10. Headless Components for Maximum Flexibility
10. 无头组件实现最大灵活性
Impact: HIGH — Logic without opinions about rendering.
Headless components provide behavior (state, keyboard handling, ARIA attributes) without any markup. Consumers supply the rendering.
tsx
function useToggle(initial = false) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(o => !o), [])
const buttonProps = {
'aria-pressed': on,
onClick: toggle,
role: 'switch' as const,
}
return { on, toggle, buttonProps }
}Usage — consumer controls all rendering:
tsx
function DarkModeSwitch() {
const { on, buttonProps } = useToggle(false)
return (
<button {...buttonProps} className={on ? 'dark' : 'light'}>
{on ? 'Dark' : 'Light'} Mode
</button>
)
}Libraries like Radix UI, Headless UI, and React Aria follow this pattern. Prefer them over fully-styled component libraries when you need design flexibility.
影响程度:高 —— 仅提供逻辑,不限制渲染方式。
无头组件提供行为(状态、键盘处理、ARIA属性)但不包含任何标记,由使用者负责渲染部分。
tsx
function useToggle(initial = false) {
const [on, setOn] = useState(initial)
const toggle = useCallback(() => setOn(o => !o), [])
const buttonProps = {
'aria-pressed': on,
onClick: toggle,
role: 'switch' as const,
}
return { on, toggle, buttonProps }
}使用示例——使用者完全控制渲染:
tsx
function DarkModeSwitch() {
const { on, buttonProps } = useToggle(false)
return (
<button {...buttonProps} className={on ? 'dark' : 'light'}>
{on ? 'Dark' : 'Light'} Mode
</button>
)
}Radix UI、Headless UI和React Aria等库都遵循这种模式。当你需要设计灵活性时,优先选择这类库而非全样式组件库。
Source
来源
Patterns from patterns.dev — composition guidance for the broader React community.
这些模式来自patterns.dev——面向广大React社区的组合设计指南。