Agent skill
react-composition
React component composition patterns for building flexible, maintainable UIs. Covers compound components, context-based state, explicit variants, and React 19 APIs. Use when designing component APIs, refactoring prop-heavy components, or building reusable component libraries.
Install this agent skill to your Project
npx add-skill https://github.com/s-hiraoku/synapse-a2a/tree/main/plugins/synapse-a2a/skills/react-composition
SKILL.md
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
| 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 | children prop |
| Component needs imperative API | useImperativeHandle |
Anti-Patterns
| Avoid | Why | Instead |
|---|---|---|
<Component isX isY isZ /> |
Combinatorial explosion, unclear interactions | Compound components or explicit variants |
renderHeader, renderFooter |
Couples parent API to child structure | children + slot components |
| Deeply nested context providers | Performance + debugging nightmare | Colocate state with consumers, split contexts |
React.cloneElement for injection |
Fragile, breaks with wrappers | Context-based composition |
| Single mega-context for all state | Every consumer re-renders on any change | Split into StateContext + ActionsContext |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
task-planner
Guide for decomposing large tasks into a structured plan with dependency chains, managing priorities, and distributing work across agents. Outputs plan cards or delegation messages as the team contract; TodoList for personal micro-steps.
react-performance
Comprehensive React and Next.js performance optimization guide. Covers waterfall elimination, bundle size reduction, server-side optimization, re-render prevention, and rendering performance. Use when building, reviewing, or optimizing React/Next.js applications for speed.
release
Update version in pyproject.toml, plugin.json, and add changelog entry. This skill should be used when the user wants to bump the version number and update CHANGELOG.md. Triggered by /release or /version commands.
api-design
Guide API design for REST, GraphQL, gRPC, and CLI interfaces. Use this skill when designing new APIs, reviewing existing API contracts, or establishing API conventions for a project. Produces consistent, well-documented API specifications.
pr-guardian
Continuously monitor a GitHub PR for merge conflicts, CI failures, and CodeRabbit review comments, then automatically fix any issues found. Polls every 5 minutes and loops until every check is green. Use this skill whenever a PR has just been created or code has been pushed to a PR branch — it should be the default follow-up action after any PR creation or push. Also trigger on: "watch this PR", "guard this PR", "monitor CI", "keep fixing until green", "PRを監視して", "CIが通るまで 直して", /pr-guardian. When a PostToolUse hook reports that a push or PR creation just happened, proactively invoke this skill to start monitoring without waiting for the user to ask.
post-impl2
Workflow: Test workflow with non-existent agent target. . Triggered by /post-impl2 command.
Didn't find tool you were looking for?