react-composition
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseReact Composition
React组件组合
Build flexible component APIs through composition instead of configuration.
通过组合而非配置的方式构建灵活的组件API。
Core Principle
核心原则
Composition over configuration. When a component needs a new behavior, the answer is almost never "add a boolean prop." Instead, compose smaller pieces together.
tsx
// BAD: Boolean prop explosion
<Modal
hasHeader
hasFooter
hasCloseButton
isFullScreen
isDismissable
hasOverlay
centerContent
/>
// GOOD: Compose what you need
<Modal>
<Modal.Header>
<Modal.Title>Settings</Modal.Title>
<Modal.Close />
</Modal.Header>
<Modal.Body>...</Modal.Body>
<Modal.Footer>
<Button onClick={save}>Save</Button>
</Modal.Footer>
</Modal>组合优先于配置。当组件需要新增行为时,解决方案几乎从来都不是“添加一个布尔类型的props”。相反,应该将更小的组件片段组合在一起。
tsx
// 不良实践:布尔props泛滥
<Modal
hasHeader
hasFooter
hasCloseButton
isFullScreen
isDismissable
hasOverlay
centerContent
/>
// 最佳实践:按需组合
<Modal>
<Modal.Header>
<Modal.Title>Settings</Modal.Title>
<Modal.Close />
</Modal.Header>
<Modal.Body>...</Modal.Body>
<Modal.Footer>
<Button onClick={save}>Save</Button>
</Modal.Footer>
</Modal>Pattern 1: Compound Components
模式一:复合组件
Share implicit state through context. Each sub-component is independently meaningful.
tsx
// 1. Define shared context
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() {
const ctx = use(TabsContext); // React 19
if (!ctx) throw new Error('useTabs must be used within <Tabs>');
return ctx;
}
// 2. Root component owns the state
function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext>
);
}
// 3. Sub-components consume context
function TabTrigger({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
role="tab"
aria-selected={activeTab === value}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabContent({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab } = useTabs();
if (activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
// 4. Attach sub-components
Tabs.Trigger = TabTrigger;
Tabs.Content = TabContent;通过Context共享隐式状态。每个子组件都具备独立的意义。
tsx
// 1. 定义共享Context
interface TabsContextValue {
activeTab: string;
setActiveTab: (tab: string) => void;
}
const TabsContext = createContext<TabsContextValue | null>(null);
function useTabs() {
const ctx = use(TabsContext); // React 19
if (!ctx) throw new Error('useTabs must be used within <Tabs>');
return ctx;
}
// 2. 根组件管理状态
function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
const [activeTab, setActiveTab] = useState(defaultTab);
return (
<TabsContext value={{ activeTab, setActiveTab }}>
<div role="tablist">{children}</div>
</TabsContext>
);
}
// 3. 子组件消费Context
function TabTrigger({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab, setActiveTab } = useTabs();
return (
<button
role="tab"
aria-selected={activeTab === value}
onClick={() => setActiveTab(value)}
>
{children}
</button>
);
}
function TabContent({ value, children }: { value: string; children: React.ReactNode }) {
const { activeTab } = useTabs();
if (activeTab !== value) return null;
return <div role="tabpanel">{children}</div>;
}
// 4. 挂载子组件
Tabs.Trigger = TabTrigger;
Tabs.Content = TabContent;Pattern 2: Explicit Variants
模式二:显式变体
When components have distinct modes, create explicit variant components instead of boolean switches.
tsx
// BAD: Boolean modes
<Input bordered />
<Input underlined />
<Input ghost />
// GOOD: Explicit variants
<Input.Bordered placeholder="Name" />
<Input.Underlined placeholder="Name" />
<Input.Ghost placeholder="Name" />
// Implementation: shared base, variant-specific styles
function createInputVariant(className: string) {
return forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<InputBase ref={ref} className={cn(className, props.className)} {...props} />
));
}
Input.Bordered = createInputVariant('border border-gray-300 rounded-md px-3 py-2');
Input.Underlined = createInputVariant('border-b border-gray-300 px-1 py-2');
Input.Ghost = createInputVariant('bg-transparent px-3 py-2');当组件存在多种不同模式时,创建显式的变体组件而非使用布尔开关。
tsx
// 不良实践:布尔模式
<Input bordered />
<Input underlined />
<Input ghost />
// 最佳实践:显式变体
<Input.Bordered placeholder="Name" />
<Input.Underlined placeholder="Name" />
<Input.Ghost placeholder="Name" />
// 实现方式:共享基础组件,变体专属样式
function createInputVariant(className: string) {
return forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<InputBase ref={ref} className={cn(className, props.className)} {...props} />
));
}
Input.Bordered = createInputVariant('border border-gray-300 rounded-md px-3 py-2');
Input.Underlined = createInputVariant('border-b border-gray-300 px-1 py-2');
Input.Ghost = createInputVariant('bg-transparent px-3 py-2');Pattern 3: Children Over Render Props
模式三:优先使用Children而非渲染Props
Use for composition. Only use render props when the child needs data from the parent.
childrentsx
// BAD: Render prop when children would work
<Card renderHeader={() => <h2>Title</h2>} renderBody={() => <p>Content</p>} />
// GOOD: Children composition
<Card>
<Card.Header><h2>Title</h2></Card.Header>
<Card.Body><p>Content</p></Card.Body>
</Card>
// ACCEPTABLE: Render prop when child needs parent data
<Combobox>
{({ isOpen, selectedItem }) => (
<>
<Combobox.Input />
{isOpen && <Combobox.Options />}
{selectedItem && <Badge>{selectedItem.label}</Badge>}
</>
)}
</Combobox>使用进行组件组合。仅当子组件需要从父组件获取数据时,才使用渲染Props。
childrentsx
// 不良实践:可使用children时却用了渲染Props
<Card renderHeader={() => <h2>Title</h2>} renderBody={() => <p>Content</p>} />
// 最佳实践:Children组合
<Card>
<Card.Header><h2>Title</h2></Card.Header>
<Card.Body><p>Content</p></Card.Body>
</Card>
// 可接受场景:子组件需要父组件数据时使用渲染Props
<Combobox>
{({ isOpen, selectedItem }) => (
<>
<Combobox.Input />
{isOpen && <Combobox.Options />}
{selectedItem && <Badge>{selectedItem.label}</Badge>}
</>
)}
</Combobox>Pattern 4: Context Interface Design
模式四:Context接口设计
Design context interfaces with clear separation of state, actions, and metadata.
tsx
interface FormContext<T> {
// State (read-only from consumer perspective)
values: T;
errors: Record<string, string>;
touched: Record<string, boolean>;
// Actions (stable references)
setValue: (field: keyof T, value: T[keyof T]) => void;
setTouched: (field: keyof T) => void;
validate: () => boolean;
submit: () => Promise<void>;
// Metadata
isSubmitting: boolean;
isDirty: boolean;
isValid: boolean;
}设计Context接口时,清晰区分状态、操作与元数据。
tsx
interface FormContext<T> {
// 状态(从消费者视角为只读)
values: T;
errors: Record<string, string>;
touched: Record<string, boolean>;
// 操作(稳定引用)
setValue: (field: keyof T, value: T[keyof T]) => void;
setTouched: (field: keyof T) => void;
validate: () => boolean;
submit: () => Promise<void>;
// 元数据
isSubmitting: boolean;
isDirty: boolean;
isValid: boolean;
}State Lifting
状态提升
Move state into provider when siblings need access.
tsx
// BAD: Prop drilling
function Parent() {
const [selected, setSelected] = useState<string | null>(null);
return (
<>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</>
);
}
// GOOD: Shared context
function Parent() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
);
}当兄弟组件需要访问同一状态时,将状态移至Provider中。
tsx
// 不良实践:Props透传
function Parent() {
const [selected, setSelected] = useState<string | null>(null);
return (
<>
<Sidebar selected={selected} onSelect={setSelected} />
<Detail selected={selected} />
</>
);
}
// 最佳实践:共享Context
function Parent() {
return (
<SelectionProvider>
<Sidebar />
<Detail />
</SelectionProvider>
);
}React 19 APIs
React 19 API
Drop forwardRef
移除forwardRef
React 19 passes as a regular prop.
reftsx
// Before (React 18)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
));
// After (React 19)
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}React 19将作为常规props传递。
reftsx
// 之前(React 18)
const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => (
<input ref={ref} {...props} />
));
// 之后(React 19)
function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}use() Instead of useContext()
使用use()替代useContext()
tsx
// Before
const ctx = useContext(ThemeContext);
// After (React 19) — works in conditionals and loops
const ctx = use(ThemeContext);tsx
// 之前
const ctx = useContext(ThemeContext);
// 之后(React 19)—— 可在条件判断和循环中使用
const ctx = use(ThemeContext);Decision Guide
决策指南
| Situation | Pattern |
|---|---|
| Component has 3+ boolean layout props | Compound components |
| Multiple visual modes of same component | Explicit variants |
| Parent data needed in flexible child layout | Render prop |
| Siblings share state | Context provider + state lifting |
| Simple customization of a slot | |
| Component needs imperative API | |
| 场景 | 适用模式 |
|---|---|
| 组件包含3个以上布局相关的布尔props | 复合组件 |
| 同一组件存在多种视觉模式 | 显式变体 |
| 灵活的子组件布局需要父组件数据 | 渲染Props |
| 兄弟组件共享状态 | Context Provider + 状态提升 |
| 简单自定义插槽 | |
| 组件需要命令式API | |
Anti-Patterns
反模式
| Avoid | Why | Instead |
|---|---|---|
| Combinatorial explosion, unclear interactions | Compound components or explicit variants |
| Couples parent API to child structure | |
| Deeply nested context providers | Performance + debugging nightmare | Colocate state with consumers, split contexts |
| Fragile, breaks with wrappers | Context-based composition |
| Single mega-context for all state | Every consumer re-renders on any change | Split into |
| 需避免的做法 | 原因 | 替代方案 |
|---|---|---|
| 组合爆炸,交互逻辑不清晰 | 复合组件或显式变体 |
| 父组件API与子组件结构强耦合 | |
| 多层嵌套的Context Provider | 性能问题 + 调试困难 | 状态与消费者就近放置,拆分Context |
使用 | 脆弱,与包装组件不兼容 | 基于Context的组合 |
| 单一巨型Context管理所有状态 | 任何状态变更都会导致所有消费者重渲染 | 拆分为 |