8.9 KiB
React Component Patterns
React-specific implementation details for compound components. For framework-agnostic patterns, see SKILL.md.
Context Architecture
What: Compound components share state via React Context without prop drilling.
Why:
- Implicit state sharing between parts (Root → Trigger → Content)
- Clean consumer API — no manual wiring
- Nested contexts for multi-level components (Accordion → Item → Trigger)
Pattern:
- Create context with
undefineddefault - Consumer hook throws if used outside provider
- Root provides state, children consume
Essential Hooks
useControlledState
What: Unifies controlled/uncontrolled state patterns.
Why: Single implementation handles both modes with consistent API.
Behavior:
- If
valueprovided → controlled (external state) - If only
defaultValue→ uncontrolled (internal state) - Calls
onChangein both modes
Ref: Radix useControllableState
useId
What: Generate unique IDs for ARIA relationships.
Why: Labels, descriptions, and controls need matching IDs for accessibility.
Note: Built into React 18+. For earlier versions, use @reach/auto-id.
useFocusTrap
What: Trap focus within a container (modal dialogs).
Why: Modal accessibility requires focus stays within dialog until closed.
Behavior:
- Tab at last element → first element
- Shift+Tab at first → last element
- Returns focus to trigger on close
Ref: focus-trap library
useRovingFocus
What: Arrow key navigation within groups with single Tab stop.
Why: Standard keyboard pattern for menus, tablists, toolbars.
Behavior:
- Only focused item has
tabIndex={0} - Others have
tabIndex={-1} - Arrows move focus, optionally loops
Ref: Radix RovingFocus
useFloating
What: Position floating elements relative to anchors.
Why: Popups need collision detection, scroll tracking, arrow positioning.
Use: Wrap @floating-ui/react with component-specific defaults.
Ref: Floating UI React
Ref Patterns
Forward Refs on All Parts
What: Every compound component part forwards refs to its DOM element.
Why: Consumers need DOM access for focus management, measurements, animations.
Pattern: forwardRef<HTMLElement, Props>((props, ref) => ...)
useImperativeHandle for Actions
What: Expose component actions through ref.
Why: Programmatic control — dialogRef.current.open()
Pattern:
interface Actions {
open(): void;
close(): void;
}
useImperativeHandle(actionsRef, () => ({ open, close }));
Ref: React useImperativeHandle
useMergeRefs / composeRefs
What: Combine multiple refs pointing to same element.
Why: Compound components often need both:
- Internal ref (for positioning, focus management, measurements)
- Forwarded ref (for consumer access)
- Floating UI ref (for anchor positioning)
Use cases:
- Trigger needs internal ref + forwarded ref + floating anchor ref
- Popup needs internal ref + forwarded ref + floating ref
- Any part using
useFloatingalongsideforwardRef
Ref: Floating UI useMergeRefs, Radix composeRefs
Render Prop Implementation
What: The render prop replaces default element with custom element or component.
Key pieces:
- Accept
ReactElementor(props, state) => ReactElement - Use
cloneElementfor element form - Use
mergePropsto combine internal + external props - Do not accept component references (
render={Component}) — calling components as plain functions breaks hooks reconciliation
mergeProps behavior:
- Event handlers — chain (both called)
- className — concatenate
- style — shallow merge
- Other props — override
Ref: Base UI useRender
Render Delegation
What: Replace default rendered element while preserving component behavior.
Why:
- Element polymorphism (button → link)
- Integrate with existing component libraries
- Conditional rendering based on internal state
Approaches
| Pattern | Library | Usage |
|---|---|---|
render prop |
Base UI | render={<a href="..." />} or render={(props, state) => ...} |
asChild prop |
Radix | <Trigger asChild><a href="...">Link</a></Trigger> |
as prop |
Various | <Button as="a" href="..."> — simpler, less flexible |
Key Utility: mergeProps
What: Safely combines props from component internals + consumer.
Behavior:
- Event handlers → chained (both called)
className→ concatenatedstyle→ shallow merged- Other props → consumer overrides
Ref: Base UI mergeProps, Radix Slot
Portal
What: Render children into document.body (or custom container).
Why: Popups need to escape parent overflow/stacking contexts.
Implementation: createPortal(children, container) after mount.
Ref: React createPortal
Server Components
What: Compound components are Client Components (use hooks, events).
Why: Interactive components can't be Server Components.
Pattern: Mark with 'use client' directive. Server Components can compose them.
TypeScript Patterns
Namespaced Types
What: Export types under component namespace.
Why: Clean imports — Dialog.RootProps, Dialog.TriggerProps
Pattern:
export namespace Dialog {
export interface RootProps { ... }
export interface TriggerProps { ... }
}
Generic Collections
What: Collection components generic over item type.
Why: Type-safe value and onValueChange for any item type.
Pattern: function Select<T>({ value, onValueChange }: SelectProps<T>)
Polymorphic Components
What: Components accepting as prop with full type inference.
Why: <Button as="a" href="..."> with correct HTML attributes.
Ref: Radix Polymorphic
Performance
Context Splitting
What: Separate contexts by update frequency.
Why: Prevent unnecessary re-renders — highlight changes shouldn't re-render entire tree.
Pattern: MenuStateContext (stable) vs MenuHighlightContext (frequent updates)
Memoization
What: useMemo for context values, memo for parts.
Why: Stable references prevent child re-renders.
Export Pattern
What: Named exports as namespace object.
Pattern:
// dialog/index.ts
export { DialogRoot as Root } from './root';
export { DialogTrigger as Trigger } from './trigger';
// Usage: import * as Dialog from './dialog';
Implementation Sources
| Pattern | Reference |
|---|---|
| Context + Compound | Base UI Dialog |
| Controlled State | Radix useControllableState |
| Focus Trap | focus-trap-react |
| Roving Focus | Radix RovingFocus |
| Floating | Floating UI React |
| Merge Props | Base UI mergeProps |
See Also
- aria/react.md — React accessibility patterns (focus scope, announcements, a11y testing)