mcpField
R
react-composition

react-composition

github.com/s-hiraoku开发1780 次安装2 周前更新
代码仓库

关于 react-composition

面向 React 开发者的组件设计技能,强调通过组合小型组件构建灵活 API,而非不断增加配置属性。内容涵盖复合组件、上下文设计及 React 19 API。

主要功能

  1. 01以组合替代布尔属性配置
  2. 02使用明确的组件变体
  3. 03通过 children 组织内容
  4. 04设计状态与操作分离的上下文
  5. 05采用 React 19 的 ref 与 use API

适用场景

  1. 01重构布尔属性过多的组件
  2. 02为组件不同模式设计 API
  3. 03让兄弟组件共享状态
  4. 04更新代码以使用 React 19 API

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/s-hiraoku/synapse-a2a --skill react-composition

安装后在 Claude Code 中输入/react-composition确认已加载。

详细介绍

React 组合

通过组合而非配置,构建灵活的组件 API。

核心原则

组合优于配置。 当组件需要新增行为时,答案几乎从来都不是“添加一个布尔型 prop”。相反,应将更小的部分组合起来。

// 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>

模式 1:复合组件

通过上下文共享隐式状态。每个子组件都有独立的意义。

// 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;

模式 2:显式变体

当组件有不同模式时,创建显式的变体组件,而不是使用布尔开关。

// 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');

模式 3:优先使用 Children,而非 Render Props

使用 children 进行组合。只有当子组件需要从父组件获取数据时,才使用 render props。

// 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>

模式 4:上下文接口设计

设计上下文接口时,清晰地区分状态、操作和元数据。

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;
}
状态提升

当兄弟组件需要访问状态时,将状态移至 provider 中。

// 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>
  );
}

React 19 API

移除 forwardRef

React 19 将 ref 作为常规 prop 传递。

// 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} />;
}
使用 use() 代替 useContext()
// Before
const ctx = useContext(ThemeContext);

// After (React 19) — works in conditionals and loops
const ctx = use(ThemeContext);

决策指南

情况模式
组件有 3 个或更多布尔型布局 prop复合组件
同一组件有多种视觉模式显式变体
灵活的子组件布局需要父组件数据Render prop
兄弟组件共享状态Context provider + 状态提升
简单自定义插槽children prop
组件需要命令式 APIuseImperativeHandle

反模式

避免原因替代方案
<Component isX isY isZ />组合爆炸,交互关系不清晰复合组件或显式变体
renderHeader、renderFooter使父组件 API 与子组件结构耦合children + 插槽组件
深层嵌套的上下文 provider性能问题 + 调试噩梦将状态与使用者放在一起,拆分上下文
使用 React.cloneElement 注入内容脆弱,遇到包装组件时会出错基于上下文的组合
所有状态都放在单个巨型上下文中任何变化都会导致所有使用者重新渲染拆分为 StateContext + ActionsContext

评论

支持 Markdown