mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
318 lines
8.9 KiB
Markdown
318 lines
8.9 KiB
Markdown
# React Component Patterns
|
|
|
|
React-specific implementation details for compound components. For framework-agnostic patterns, see [SKILL.md](../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 `undefined` default
|
|
- Consumer hook throws if used outside provider
|
|
- Root provides state, children consume
|
|
|
|
**Ref:** [Base UI Dialog Source](https://github.com/mui/base-ui/tree/master/packages/react/src/dialog)
|
|
|
|
---
|
|
|
|
## Essential Hooks
|
|
|
|
### `useControlledState`
|
|
|
|
**What:** Unifies controlled/uncontrolled state patterns.
|
|
|
|
**Why:** Single implementation handles both modes with consistent API.
|
|
|
|
**Behavior:**
|
|
|
|
- If `value` provided → controlled (external state)
|
|
- If only `defaultValue` → uncontrolled (internal state)
|
|
- Calls `onChange` in both modes
|
|
|
|
**Ref:** [Radix useControllableState](https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx)
|
|
|
|
---
|
|
|
|
### `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](https://github.com/focus-trap/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](https://github.com/radix-ui/primitives/tree/main/packages/react/roving-focus)
|
|
|
|
---
|
|
|
|
### `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](https://floating-ui.com/docs/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:**
|
|
|
|
```tsx
|
|
interface Actions {
|
|
open(): void;
|
|
close(): void;
|
|
}
|
|
useImperativeHandle(actionsRef, () => ({ open, close }));
|
|
```
|
|
|
|
**Ref:** [React useImperativeHandle](https://react.dev/reference/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 `useFloating` alongside `forwardRef`
|
|
|
|
**Ref:** [Floating UI useMergeRefs](https://floating-ui.com/docs/react#usemergerefs), [Radix composeRefs](https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/composeRefs.tsx)
|
|
|
|
---
|
|
|
|
## Render Prop Implementation
|
|
|
|
**What:** The `render` prop replaces default element with custom element or component.
|
|
|
|
**Key pieces:**
|
|
|
|
- Accept `ReactElement` or `(props, state) => ReactElement`
|
|
- Use `cloneElement` for element form
|
|
- Use `mergeProps` to 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](https://github.com/mui/base-ui/blob/master/packages/react/src/use-render/useRender.ts)
|
|
|
|
---
|
|
|
|
## 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` → concatenated
|
|
- `style` → shallow merged
|
|
- Other props → consumer overrides
|
|
|
|
**Ref:** [Base UI mergeProps](https://github.com/mui/base-ui/blob/master/packages/react/src/merge-props/mergeProps.ts), [Radix Slot](https://github.com/radix-ui/primitives/tree/main/packages/react/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](https://react.dev/reference/react-dom/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:**
|
|
|
|
```tsx
|
|
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](https://github.com/radix-ui/primitives/tree/main/packages/react/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:**
|
|
|
|
```tsx
|
|
// 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](https://github.com/mui/base-ui/tree/master/packages/react/src/dialog) |
|
|
| Controlled State | [Radix useControllableState](https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state) |
|
|
| Focus Trap | [focus-trap-react](https://github.com/focus-trap/focus-trap-react) |
|
|
| Roving Focus | [Radix RovingFocus](https://github.com/radix-ui/primitives/tree/main/packages/react/roving-focus) |
|
|
| Floating | [Floating UI React](https://floating-ui.com/docs/react) |
|
|
| Merge Props | [Base UI mergeProps](https://github.com/mui/base-ui/blob/master/packages/react/src/merge-props/mergeProps.ts) |
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [aria/react.md](../../aria/references/react.md) — React accessibility patterns (focus scope, announcements, a11y testing)
|