react-composition

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React 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
children
for composition. Only use render props when the child needs data from the parent.
tsx
// 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>
使用
children
进行组件组合。仅当子组件需要从父组件获取数据时,才使用渲染Props。
tsx
// 不良实践:可使用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
ref
as a regular prop.
tsx
// 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将
ref
作为常规props传递。
tsx
// 之前(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

决策指南

SituationPattern
Component has 3+ boolean layout propsCompound components
Multiple visual modes of same componentExplicit variants
Parent data needed in flexible child layoutRender prop
Siblings share stateContext provider + state lifting
Simple customization of a slot
children
prop
Component needs imperative API
useImperativeHandle
场景适用模式
组件包含3个以上布局相关的布尔props复合组件
同一组件存在多种视觉模式显式变体
灵活的子组件布局需要父组件数据渲染Props
兄弟组件共享状态Context Provider + 状态提升
简单自定义插槽
children
props
组件需要命令式API
useImperativeHandle

Anti-Patterns

反模式

AvoidWhyInstead
<Component isX isY isZ />
Combinatorial explosion, unclear interactionsCompound components or explicit variants
renderHeader
,
renderFooter
Couples parent API to child structure
children
+ slot components
Deeply nested context providersPerformance + debugging nightmareColocate state with consumers, split contexts
React.cloneElement
for injection
Fragile, breaks with wrappersContext-based composition
Single mega-context for all stateEvery consumer re-renders on any changeSplit into
StateContext
+
ActionsContext
需避免的做法原因替代方案
<Component isX isY isZ />
组合爆炸,交互逻辑不清晰复合组件或显式变体
renderHeader
,
renderFooter
父组件API与子组件结构强耦合
children
+ 插槽组件
多层嵌套的Context Provider性能问题 + 调试困难状态与消费者就近放置,拆分Context
使用
React.cloneElement
注入数据
脆弱,与包装组件不兼容基于Context的组合
单一巨型Context管理所有状态任何状态变更都会导致所有消费者重渲染拆分为
StateContext
+
ActionsContext