mcpField
R
react-composition

react-composition

github.com/s-hiraokuDevelopment1780 installsUpdated 3w ago
Repository

About react-composition

A component-design skill for React developers that favors composing small pieces over adding configuration props. It covers compound components, context design, and React 19 APIs.

Key features

  1. 01Favor composition over boolean props
  2. 02Use explicit component variants
  3. 03Compose content with children
  4. 04Separate state and actions in context
  5. 05Apply React 19 ref and use APIs

Use cases

  1. 01Refactor components with many boolean props
  2. 02Design APIs for distinct component modes
  3. 03Share state between sibling components
  4. 04Update code to use React 19 APIs

Install

Copy the command and run it in a terminal.

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

After installing, type/react-compositionin Claude Code to confirm it loaded.

Details

From the project README

React Composition

Build flexible component APIs through composition instead of configuration.

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.

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

Pattern 1: Compound Components

Share implicit state through context. Each sub-component is independently meaningful.

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

Pattern 2: Explicit Variants

When components have distinct modes, create explicit variant components instead of boolean switches.

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

Pattern 3: Children Over Render Props

Use children for composition. Only use render props when the child needs data from the parent.

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

Pattern 4: Context Interface Design

Design context interfaces with clear separation of state, actions, and metadata.

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;
}
State Lifting

Move state into provider when siblings need access.

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

Drop forwardRef

React 19 passes ref as a regular 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() Instead of useContext()
// Before
const ctx = useContext(ThemeContext);

// After (React 19) — works in conditionals and loops
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 slotchildren prop
Component needs imperative APIuseImperativeHandle

Anti-Patterns

AvoidWhyInstead
<Component isX isY isZ />Combinatorial explosion, unclear interactionsCompound components or explicit variants
renderHeader, renderFooterCouples parent API to child structurechildren + slot components
Deeply nested context providersPerformance + debugging nightmareColocate state with consumers, split contexts
React.cloneElement for injectionFragile, breaks with wrappersContext-based composition
Single mega-context for all stateEvery consumer re-renders on any changeSplit into StateContext + ActionsContext

Comments

Markdown supported