SPB Git

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%

# Patterns — Building React Components

# Contents

  • Props API: variant unions and discriminated props
  • Composition: compound components
  • State placement: the decision table
  • Controlled vs uncontrolled inputs
  • Custom hooks
  • Measured memoization
  • Gotchas

# Props API: variant unions and discriminated props

tsx
type ButtonProps = {
  variant?: 'primary' | 'danger' | 'ghost';   // one axis, one prop
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;                          // true independent boolean is fine
  onClick: () => void;
  children: React.ReactNode;
};

export function Button({ variant = 'primary', size = 'md', ...rest }: ButtonProps) { /* … */ }

Discriminated union when props only make sense together:

tsx
type AlertProps =
  | { kind: 'info'; message: string }
  | { kind: 'error'; message: string; retry: () => void }; // retry exists only for errors

# Composition: compound components

tsx
export function Card({ children }: { children: React.ReactNode }) {
  return <section className="card">{children}</section>;
}
Card.Header = function CardHeader({ children }: { children: React.ReactNode }) {
  return <header className="card-header">{children}</header>;
};
Card.Body = function CardBody({ children }: { children: React.ReactNode }) {
  return <div className="card-body">{children}</div>;
};

// Usage — caller controls content, Card controls chrome:
<Card>
  <Card.Header><h2>Invoices</h2></Card.Header>
  <Card.Body><InvoiceTable rows={rows} /></Card.Body>
</Card>

Escape hatch: if slots must be validated/reordered, accept named element props (header={<h2>…</h2>}) — still elements, not strings-plus-config.

# State placement: the decision table

The value is… Home Tool
Fetched from an API Query cache TanStack Query / SWR
Shareable via link (filters, tab, page) URL useSearchParams
Form input mid-edit Local component useState / form library
Theme, auth, locale (rarely changes, read widely) Context createContext
Everything else Lowest component that uses it useState

# Controlled vs uncontrolled inputs

tsx
// Controlled — other UI reacts per keystroke
const [query, setQuery] = useState('');
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<Results filter={query} />

// Uncontrolled — value only needed on submit
<form onSubmit={(e) => {
  e.preventDefault();
  const data = new FormData(e.currentTarget);
  save(data.get('email'));
}}>
  <input name="email" defaultValue={user.email} />
</form>

# Custom hooks

tsx
// Reusable logic = hook. Name starts with `use`, returns plain values.
function useDebouncedValue<T>(value: T, delayMs = 300) {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delayMs);
    return () => clearTimeout(id);
  }, [value, delayMs]);
  return debounced;
}

const debouncedQuery = useDebouncedValue(query); // any component, same behavior

# Measured memoization

tsx
// 1. Measure first: React DevTools Profiler → record → find components
//    re-rendering with unchanged props AND non-trivial render cost.
// 2. Then, and only then:
const Row = React.memo(function Row({ item }: { item: Item }) { /* … */ });
// Parent must keep prop identities stable for memo to work:
const onSelect = useCallback((id: string) => setSelected(id), []);
// Comment the reason so the next reader knows it's load-bearing:
// memo: 5k rows re-rendered on every keystroke before this (Profiler 2026-08).

# Gotchas

  • useEffect for derivation is the top React bug source — if the effect only calls setState from other state/props, delete it and compute during render.
  • Index as key breaks reordering/deletion — use a stable id; index is acceptable only for static, never-reordered lists.
  • Object/array literals in JSX props (style={{…}}, options={[…]}) defeat React.memo children — hoist or memoize them only where memo is in play.
  • Context triggers re-render of every consumer on any value change — split contexts (state vs dispatch) or keep fast-changing values out of context.
  • useCallback without a memoized child is pure overhead — it saves nothing.
  • Stale closure in intervals/subscriptions: include deps or use a ref; an empty dep array freezes captured state at mount time.
  • 'use client' at the page root opts the whole tree out of server rendering — place it at the interactive leaf component instead.