spb/ultra-sharp-agent-skills Public
Ultra-Sharp Agent Skills — a research-first skill-authoring system + 72 production-ready skills for AI agents.
Python 100%
1<!--2Author: Simon-Pierre Boucher3Contact: contact@spboucher.ai4-->56# Patterns — Building React Components78## Contents9- Props API: variant unions and discriminated props10- Composition: compound components11- State placement: the decision table12- Controlled vs uncontrolled inputs13- Custom hooks14- Measured memoization15- Gotchas1617## Props API: variant unions and discriminated props1819```tsx20type ButtonProps = {21 variant?: 'primary' | 'danger' | 'ghost'; // one axis, one prop22 size?: 'sm' | 'md' | 'lg';23 disabled?: boolean; // true independent boolean is fine24 onClick: () => void;25 children: React.ReactNode;26};2728export function Button({ variant = 'primary', size = 'md', ...rest }: ButtonProps) { /* … */ }29```3031Discriminated union when props only make sense together:3233```tsx34type AlertProps =35 | { kind: 'info'; message: string }36 | { kind: 'error'; message: string; retry: () => void }; // retry exists only for errors37```3839## Composition: compound components4041```tsx42export function Card({ children }: { children: React.ReactNode }) {43 return <section className="card">{children}</section>;44}45Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {46 return <header className="card-header">{children}</header>;47};48Card.Body = function CardBody({ children }: { children: React.ReactNode }) {49 return <div className="card-body">{children}</div>;50};5152// Usage — caller controls content, Card controls chrome:53<Card>54 <Card.Header><h2>Invoices</h2></Card.Header>55 <Card.Body><InvoiceTable rows={rows} /></Card.Body>56</Card>57```5859Escape hatch: if slots must be validated/reordered, accept named element props60(`header={<h2>…</h2>}`) — still elements, not strings-plus-config.6162## State placement: the decision table6364| The value is… | Home | Tool |65|---|---|---|66| Fetched from an API | Query cache | TanStack Query / SWR |67| Shareable via link (filters, tab, page) | URL | `useSearchParams` |68| Form input mid-edit | Local component | `useState` / form library |69| Theme, auth, locale (rarely changes, read widely) | Context | `createContext` |70| Everything else | Lowest component that uses it | `useState` |7172## Controlled vs uncontrolled inputs7374```tsx75// Controlled — other UI reacts per keystroke76const [query, setQuery] = useState('');77<input value={query} onChange={(e) => setQuery(e.target.value)} />78<Results filter={query} />7980// Uncontrolled — value only needed on submit81<form onSubmit={(e) => {82 e.preventDefault();83 const data = new FormData(e.currentTarget);84 save(data.get('email'));85}}>86 <input name="email" defaultValue={user.email} />87</form>88```8990## Custom hooks9192```tsx93// Reusable logic = hook. Name starts with `use`, returns plain values.94function useDebouncedValue<T>(value: T, delayMs = 300) {95 const [debounced, setDebounced] = useState(value);96 useEffect(() => {97 const id = setTimeout(() => setDebounced(value), delayMs);98 return () => clearTimeout(id);99 }, [value, delayMs]);100 return debounced;101}102103const debouncedQuery = useDebouncedValue(query); // any component, same behavior104```105106## Measured memoization107108```tsx109// 1. Measure first: React DevTools Profiler → record → find components110// re-rendering with unchanged props AND non-trivial render cost.111// 2. Then, and only then:112const Row = React.memo(function Row({ item }: { item: Item }) { /* … */ });113// Parent must keep prop identities stable for memo to work:114const onSelect = useCallback((id: string) => setSelected(id), []);115// Comment the reason so the next reader knows it's load-bearing:116// memo: 5k rows re-rendered on every keystroke before this (Profiler 2026-08).117```118119## Gotchas120121- **`useEffect` for derivation** is the top React bug source — if the effect only122 calls `setState` from other state/props, delete it and compute during render.123- **Index as `key`** breaks reordering/deletion — use a stable id; index is124 acceptable only for static, never-reordered lists.125- **Object/array literals in JSX props** (`style={{…}}`, `options={[…]}`) defeat126 `React.memo` children — hoist or memoize them only where memo is in play.127- **Context triggers re-render of every consumer** on any value change — split128 contexts (state vs dispatch) or keep fast-changing values out of context.129- **`useCallback` without a memoized child** is pure overhead — it saves nothing.130- **Stale closure in intervals/subscriptions**: include deps or use a ref;131 an empty dep array freezes captured state at mount time.132- **`'use client'` at the page root** opts the whole tree out of server133 rendering — place it at the interactive leaf component instead.134