Agent SkillsPatternsDev/skills › react-composition-2026

react-composition-2026

GitHub

教授现代 React 组合模式,通过用组合替代布尔属性、构建复合组件等方式,解决组件 API 臃肿和状态管理复杂问题,提升 UI 库的可维护性与扩展性。

react/react-composition-2026/SKILL.md PatternsDev/skills

Trigger Scenarios

设计组件 API 构建共享 UI 库 重构高复杂度组件

Install

npx skills add PatternsDev/skills --skill react-composition-2026 -g -y
More Options

Non-standard path

npx skills add https://github.com/PatternsDev/skills/tree/main/react/react-composition-2026 -g -y

Use without installing

npx skills use PatternsDev/skills@react-composition-2026

指定 Agent (Claude Code)

npx skills add PatternsDev/skills --skill react-composition-2026 -a claude-code -g -y

安装 repo 全部 skill

npx skills add PatternsDev/skills --all -g -y

预览 repo 内 skill

npx skills add PatternsDev/skills --list

SKILL.md

Frontmatter
{
    "name": "react-composition-2026",
    "paths": [
        "**\/*.tsx",
        "**\/*.jsx"
    ],
    "context": "fork",
    "license": "MIT",
    "metadata": {
        "author": "patterns.dev",
        "version": "1.1"
    },
    "description": "Teaches modern React composition patterns for 2025\/2026. Use when designing component APIs, building shared UI libraries, or refactoring prop-heavy components.",
    "allowed-tools": "Read, Grep, Glob",
    "related_skills": [
        "hooks-pattern",
        "hoc-pattern"
    ]
}

Modern React Composition Patterns

Table of Contents

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.

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

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.

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.


1. Replace Boolean Props with Composition

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:

<Card
  showHeader
  showFooter
  collapsible
  bordered
  withShadow
  headerAction="close"
  size="large"
/>

Prefer — explicit composition:

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


2. Build Compound Components with 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:

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

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

Usage:

<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

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:

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:

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.


4. Use Children Over Render Props for Composition

Impact: MEDIUM — Simpler API, better readability.

Render props (renderHeader, renderItem) were essential before hooks, but today children provides cleaner composition for most cases.

Avoid — render prop proliferation:

<DataTable
  data={users}
  renderHeader={() => <h2>Users</h2>}
  renderRow={(user) => <UserRow user={user} />}
  renderEmpty={() => <EmptyState />}
  renderFooter={() => <Pagination />}
/>

Prefer — children composition:

<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).


5. Decouple State Implementation from 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:

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:

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:

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 useCounter() consumers never change.


6. Lift State to Provider Components

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:

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:

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 Sidebar and Detail consume useSelection() directly.


7. Use Polymorphic as Props for Flexible Elements

Impact: MEDIUM — One component, any underlying element or component.

The as prop pattern lets consumers control the rendered element while keeping your component's styles and behavior.

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:

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

8. React 19: Drop forwardRef, Use ref as a Prop

Impact: MEDIUM — Simpler component definitions.

React 19 passes ref as a regular prop. No more forwardRef wrapper.

React 18 (deprecated pattern):

const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
  return <input ref={ref} {...props} />
})

React 19:

function Input({ ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} {...props} />
}

Similarly, use() can read either promises or context and can be called conditionally:

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

9. Slot Pattern for Layout Components

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.

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:

<AppLayout>
  <AppLayout.Header>
    <Logo />
    <Nav />
  </AppLayout.Header>
  <Dashboard />
</AppLayout>

10. Headless Components for Maximum Flexibility

Impact: HIGH — Logic without opinions about rendering.

Headless components provide behavior (state, keyboard handling, ARIA attributes) without any markup. Consumers supply the rendering.

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:

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.


Source

Patterns from patterns.dev — composition guidance for the broader React community.

Version History

  • 48bf58a Current 2026-07-24 16:35

Same Skill Collection

javascript/bundle-splitting/SKILL.md
javascript/command-pattern/SKILL.md
javascript/compression/SKILL.md
javascript/dynamic-import/SKILL.md
javascript/factory-pattern/SKILL.md
javascript/flyweight-pattern/SKILL.md
javascript/import-on-interaction/SKILL.md
javascript/import-on-visibility/SKILL.md
javascript/islands-architecture/SKILL.md
javascript/js-performance-patterns/SKILL.md
javascript/loading-sequence/SKILL.md
javascript/mediator-pattern/SKILL.md
javascript/mixin-pattern/SKILL.md
javascript/module-pattern/SKILL.md
javascript/observer-pattern/SKILL.md
javascript/prefetch/SKILL.md
javascript/preload/SKILL.md
javascript/prototype-pattern/SKILL.md
javascript/provider-pattern/SKILL.md
javascript/proxy-pattern/SKILL.md
javascript/prpl/SKILL.md
javascript/route-based/SKILL.md
javascript/singleton-pattern/SKILL.md
javascript/static-import/SKILL.md
javascript/third-party/SKILL.md
javascript/tree-shaking/SKILL.md
javascript/view-transitions/SKILL.md
javascript/virtual-lists/SKILL.md
javascript/vite-bundle-optimization/SKILL.md
react/ai-ui-patterns/SKILL.md
react/client-side-rendering/SKILL.md
react/compound-pattern/SKILL.md
react/hoc-pattern/SKILL.md
react/hooks-pattern/SKILL.md
react/incremental-static-rendering/SKILL.md
react/presentational-container-pattern/SKILL.md
react/progressive-hydration/SKILL.md
react/react-2026/SKILL.md
react/react-data-fetching/SKILL.md
react/react-render-optimization/SKILL.md
react/react-selective-hydration/SKILL.md
react/react-server-components/SKILL.md
react/render-props-pattern/SKILL.md
react/server-side-rendering/SKILL.md
react/static-rendering/SKILL.md
react/streaming-ssr/SKILL.md
vue/async-components/SKILL.md
vue/components/SKILL.md
vue/composables/SKILL.md

Metadata

Files
0
Version
48bf58a
Hash
9437698e
Indexed
2026-07-24 16:35

inicio - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-20 06:36
浙ICP备14020137号-1 $mapa de visitantes$