Claude Skill

react-ops

React development patterns, hooks, state management, Server Components, and performance optimization. Use for: react, hooks, useState, useEffect, jsx, tsx, server components, RSC, zustand, react query, component patterns, react testing library, error boundary, suspense, react 19.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download 0xdarkmatter-claude-mods-skills_react-ops-3dfaf0b.zip · 46 KB
Part of 0xdarkmatter/claude-mods — 94 skills

Install

skills CLI npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/react-ops
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
Git git clone https://github.com/0xDarkMatter/claude-mods.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

React Operations

Comprehensive React skill covering hooks, component architecture, state management, Server Components, and performance optimization.

React 19 ecosystem facts verified as of 2026-07.

Hook Selection Decision Tree

What problem are you solving?
│
├─ Storing UI state that triggers re-renders
│  ├─ Simple value (string, number, boolean)
│  │  └─ useState
│  ├─ Complex state with multiple sub-values and logic
│  │  └─ useReducer (actions + reducer = predictable transitions)
│  └─ Derived from existing state
│     └─ Calculate inline or useMemo — not useState
│
├─ Referencing a value WITHOUT triggering re-render
│  ├─ DOM element reference
│  │  └─ useRef<HTMLElement>(null) + ref={ref}
│  └─ Mutable value (timer ID, previous value, counter)
│     └─ useRef (mutate ref.current directly)
│
├─ Running a side effect
│  ├─ After every render (or specific deps)
│  │  ├─ Needs cleanup (subscription, timer, abort)
│  │  │  └─ useEffect with return cleanup function
│  │  └─ No cleanup (logging, analytics)
│  │     └─ useEffect with empty or dep array
│  ├─ Before browser paint (DOM mutation, animation)
│  │  └─ useLayoutEffect
│  └─ Triggered by user action (not render)
│     └─ Call it directly in the event handler — not useEffect
│
├─ Caching an expensive computation
│  └─ useMemo(() => expensiveCalc(a, b), [a, b])
│
├─ Stable callback reference for child props / event handlers
│  └─ useCallback(() => doThing(dep), [dep])
│
├─ Reading shared context value
│  └─ useContext(MyContext)
│
├─ Generating stable unique ID (forms, aria)
│  └─ useId()
│
├─ Syncing external store (Redux, Zustand internals)
│  └─ useSyncExternalStore(subscribe, getSnapshot)
│
└─ React 19+
   ├─ Await a promise or read context
   │  └─ use(promise | context)
   ├─ Form submit state (pending, data, action)
   │  └─ useFormStatus / useActionState
   └─ Optimistic UI before server response
      └─ useOptimistic(state, updateFn)

Component Pattern Decision Tree

What's your composition challenge?
│
├─ Group of related components sharing implicit state
│  (Tabs, Accordion, Select, Menu)
│  └─ Compound Components with Context
│     Parent provides state via Context
│     Children consume via useContext
│
├─ Consumer needs to control rendering output
│  └─ Render Props: children(props) or render={fn}
│     Good for: headless UI, flexible layouts
│
├─ Apply cross-cutting concerns (auth, logging, theming)
│  to multiple components
│  └─ Higher-Order Components (HOC)
│     Wrap with withAuth(Component) or withLogging(Component)
│     Prefer custom hooks for pure logic
│
├─ Encapsulate reusable stateful logic
│  └─ Custom Hook — always prefer over HOC when possible
│     Composable, testable, no wrapper hell
│
├─ Need imperative control from parent (focus, scroll, reset)
│  └─ forwardRef + useImperativeHandle
│
├─ Render content outside DOM hierarchy (modal, tooltip, toast)
│  └─ Portal: createPortal(content, document.body)
│
├─ Accept arbitrary children/slots without prop drilling
│  └─ Slot pattern via children, or named props (header, footer)
│
└─ Polymorphic rendering (button that renders as <a> or div)
   └─ as prop pattern with TypeScript generics

State Management Decision Tree

Where does this state live and who owns it?
│
├─ Only one component needs it
│  └─ useState or useReducer (local state)
│
├─ A few nearby components need it
│  └─ Lift state to nearest common ancestor + prop drilling
│     (2-3 levels is fine)
│
├─ Many components need it, rarely changes
│  (theme, locale, auth user)
│  └─ React Context API
│     Split contexts by update frequency
│     Avoid single giant context
│
├─ Global client state, changes often
│  (shopping cart, UI preferences, navigation)
│  ├─ Simple/small app → Zustand (minimal boilerplate)
│  ├─ Atomic updates, React Suspense integration → Jotai
│  └─ Large team, time-travel debugging, complex logic → Redux Toolkit
│
├─ Server state (remote data, cache, sync)
│  (API data, database queries)
│  └─ TanStack Query (React Query)
│     Handles: caching, background refetch, loading/error
│     Don't use useState + useEffect for server data
│
└─ Form state
   └─ React Hook Form + Zod validation
      (controlled inputs are fine for simple forms)

React 19 Quick Reference

Feature API Purpose
use() hook use(promise) / use(context) Await promises in render, read context conditionally
Actions async function action(formData) Async transitions with built-in pending state
useActionState useActionState(action, initialState) Action result + pending state
useFormStatus useFormStatus() Pending/data/method inside form
useOptimistic useOptimistic(state, updateFn) Optimistic UI before server response
React Compiler Automatic memoization Replaces most memo, useMemo, useCallback
ref as prop <Input ref={ref}> No more forwardRef wrapper needed
<Context> as provider <MyContext value={val}> No more <MyContext.Provider>
// React 19: use() for data fetching in Server Components
import { use } from 'react';

function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // suspends until resolved
  return <h1>{user.name}</h1>;
}

// React 19: useActionState
import { useActionState } from 'react';

function ContactForm() {
  const [state, action, isPending] = useActionState(
    async (prevState: State, formData: FormData) => {
      const result = await submitContact(formData);
      return result;
    },
    { error: null }
  );

  return (
    <form action={action}>
      <input name="email" type="email" />
      <button disabled={isPending}>
        {isPending ? 'Sending...' : 'Send'}
      </button>
      {state.error && <p>{state.error}</p>}
    </form>
  );
}

Server vs Client Components

Does this component need...?
│
├─ useState, useReducer, useContext
│  └─ Client Component ('use client')
│
├─ useEffect, useLayoutEffect
│  └─ Client Component ('use client')
│
├─ Browser APIs (window, document, localStorage)
│  └─ Client Component ('use client')
│
├─ Event handlers (onClick, onChange, onSubmit)
│  └─ Client Component ('use client')
│
├─ Third-party libraries that use hooks/browser APIs
│  └─ Client Component ('use client')
│
├─ Direct database/file system access
│  └─ Server Component (default, no directive)
│
├─ Access to env vars (server-only secrets)
│  └─ Server Component
│
├─ Large dependencies you want to keep off the client bundle
│  └─ Server Component
│
└─ async/await at the top level
   └─ Server Component

Client boundary rules:

  • 'use client' marks a boundary — everything imported below it becomes client JS
  • Server Components can import Client Components (they pass as props/children)
  • Client Components CANNOT import Server Components directly
  • Pass Server Component output as children prop to Client Components
  • Server data → Client: pass as serializable props only (no functions, classes, DOM nodes)

Performance Checklist

Technique When to Use When NOT to Use
React.memo Component re-renders often with same props Nearly everything — adds comparison overhead
useMemo Expensive calculation (>1ms), stable dep array Primitive values, simple expressions
useCallback Callback passed to memoized child or in dep array Inline handlers on DOM elements
React.lazy + Suspense Large components not needed on initial load Small components, SSR-critical content
useTransition Non-urgent state updates (filtering, sorting) Time-sensitive UI (typing, hover)
useDeferredValue Derived expensive render from fast-changing value Same as above
Virtualization Lists >100 items Small lists — overhead not worth it
React Compiler (v19) Automatic — replaces most manual memoization Opt-out with "use no memo" if needed

Common Gotchas

Gotcha Why It Happens Fix
Stale closure in useEffect Callback captures old state/prop at definition time Add value to dep array, or use functional update setState(prev => ...)
Missing useEffect dependency Linter disabled or ignored, stale data shown Never disable exhaustive-deps; use useCallback to stabilize functions
Index as list key Keys change on reorder/insert, causing wrong component identity Use stable unique ID from data (item.id)
Hydration mismatch Server HTML doesn't match first client render Avoid typeof window, random values, or dates in render; use useEffect for client-only content
Unnecessary re-renders from context All consumers re-render when any context value changes Split context by concern; memoize context value with useMemo
useEffect for derived state State derived from another state causes extra render cycle Compute derived value during render inline or with useMemo
Missing cleanup in useEffect Memory leaks from subscriptions, timers, fetch requests Always return cleanup function; use AbortController for fetch
Strict Mode double invocation Effects run twice in dev to catch bugs Design effects to be idempotent; cleanup must fully reverse effect
Controlled/uncontrolled switch value prop toggling between defined and undefined Always provide defined value or always use defaultValue; never both
Object/array in dep array New reference every render triggers effect repeatedly Memoize with useMemo; use primitive values in deps where possible
Async function directly in useEffect useEffect(() => async () => {}) returns a Promise, not cleanup Wrap: useEffect(() => { async function run() {...}; run(); }, [])

Reference Files

File When to Load
./references/hooks-patterns.md Deep hook usage: custom hooks, React 19 hooks, useEffect patterns, hook composition
./references/component-architecture.md Compound components, HOC, render props, portals, forwardRef, polymorphic components
./references/state-management.md Context API, Zustand, Jotai, Redux Toolkit, TanStack Query, React Hook Form
./references/server-components.md RSC architecture, Server Actions, Next.js App Router, caching, streaming, metadata
./references/performance.md React.memo, code splitting, virtualization, React Compiler, Web Vitals, profiling
./references/testing.md RTL queries, user-event, MSW, renderHook, Vitest setup, accessibility testing

Staleness Verifier

This skill encodes fast-moving facts (the React 19 API surface, the ecosystem package stack). scripts/check-react-facts.py guards them against silent drift — internal consistency in PR CI, live major-version drift in the scheduled freshness job:

# Structural (PR CI, no network): every catalogued package + React 19 gate is
# still named in this skill's prose, and the currency note still carries a year.
python3 skills/react-ops/scripts/check-react-facts.py --offline        # exit 0 consistent, 10 drift

# Live (weekly freshness job, never blocks a PR): is any documented major
# now behind npm's latest dist-tag?
python3 skills/react-ops/scripts/check-react-facts.py --live           # exit 10 a major moved ahead, 7 npm unreachable

The canonical fact list lives in assets/react-facts.json; when you add or drop a recommendation or the prose stops naming one, update it to match or --offline fails CI.

See Also

Skill When to Combine
typescript-ops TypeScript generics with React props, discriminated unions for state machines, utility types
testing-ops Test strategy, mocking patterns, CI integration, snapshot vs behavioral tests
tailwind-ops CSS-in-JS alternatives, responsive design with Tailwind in React components
javascript-ops Async patterns, Promises, generators, module system fundamentals
Files (claude-mods)
  • assets
    • .gitkeep 0 B · in bundle
    • react-facts.json 2.3 KB
      {
        "_comment": "Canonical fast-moving facts the react-ops skill encodes. scripts/check-react-facts.py asserts SKILL.md + references name these consistently (--offline) and probes the npm registry for major-version drift (--live). Edit deliberately: a change here is a skill-content decision, not housekeeping. documented_major is the major the skill's prose commits to (react/react-dom/next) or the current tracked major for ecosystem libs the prose names without pinning a version.",
        "schema": "claude-mods.react-ops.facts/v1",
        "as_of": "2026-07-05",
        "react_major": 19,
        "version_gates": {
          "_comment": "React 19 APIs the skill centers on. --offline asserts each token is named in SKILL.md/references prose; a missing token means the skill stopped teaching a React 19 feature it claims.",
          "use": "use(",
          "actions": "Actions",
          "use_action_state": "useActionState",
          "use_form_status": "useFormStatus",
          "use_optimistic": "useOptimistic",
          "react_compiler": "React Compiler"
        },
        "packages": {
          "react":              { "documented_major": 19, "prose": ["react"],              "role": "core (React 19)" },
          "react-dom":          { "documented_major": 19, "prose": ["react-dom"],          "role": "DOM host (useFormStatus, createRoot)" },
          "next":               { "documented_major": 16, "prose": ["Next.js"],            "role": "App Router / RSC host" },
          "zustand":            { "documented_major": 5,  "prose": ["Zustand"],            "role": "client state" },
          "jotai":              { "documented_major": 2,  "prose": ["Jotai"],              "role": "atomic client state" },
          "@reduxjs/toolkit":   { "documented_major": 2,  "prose": ["Redux Toolkit"],      "role": "complex client state" },
          "@tanstack/react-query": { "documented_major": 5, "prose": ["TanStack Query"],   "role": "server state" },
          "react-hook-form":    { "documented_major": 7,  "prose": ["React Hook Form"],    "role": "form state" },
          "zod":                { "documented_major": 4,  "prose": ["Zod"],                "role": "schema validation" },
          "@testing-library/react": { "documented_major": 16, "prose": ["@testing-library/react"], "role": "component testing" },
          "vitest":             { "documented_major": 4,  "prose": ["vitest"],             "role": "test runner" }
        }
      }
      
  • references
    • component-architecture.md 15.6 KB
      # Component Architecture
      
      Patterns for structuring React components: compound components, HOC, render props, portals, refs, and polymorphic components.
      
      ---
      
      ## Compound Components
      
      Compound components share implicit state through Context. The parent owns state; children consume it without prop drilling.
      
      ```tsx
      import {
        createContext,
        useContext,
        useState,
        ReactNode,
        KeyboardEvent,
      } from 'react';
      
      // --- Types ---
      interface TabsContextValue {
        activeIndex: number;
        setActiveIndex: (index: number) => void;
      }
      
      // --- Context ---
      const TabsContext = createContext<TabsContextValue | null>(null);
      
      function useTabsContext() {
        const ctx = useContext(TabsContext);
        if (!ctx) throw new Error('Tabs sub-components must be used within <Tabs>');
        return ctx;
      }
      
      // --- Compound Components ---
      
      function Tabs({
        children,
        defaultIndex = 0,
      }: {
        children: ReactNode;
        defaultIndex?: number;
      }) {
        const [activeIndex, setActiveIndex] = useState(defaultIndex);
        return (
          <TabsContext.Provider value={{ activeIndex, setActiveIndex }}>
            <div className="tabs">{children}</div>
          </TabsContext.Provider>
        );
      }
      
      function TabList({ children }: { children: ReactNode }) {
        return (
          <div role="tablist" className="tab-list">
            {children}
          </div>
        );
      }
      
      function Tab({ children, index }: { children: ReactNode; index: number }) {
        const { activeIndex, setActiveIndex } = useTabsContext();
        const isActive = activeIndex === index;
      
        const handleKeyDown = (e: KeyboardEvent) => {
          if (e.key === 'Enter' || e.key === ' ') setActiveIndex(index);
        };
      
        return (
          <button
            role="tab"
            aria-selected={isActive}
            tabIndex={isActive ? 0 : -1}
            onClick={() => setActiveIndex(index)}
            onKeyDown={handleKeyDown}
            className={isActive ? 'tab tab--active' : 'tab'}
          >
            {children}
          </button>
        );
      }
      
      function TabPanels({ children }: { children: ReactNode }) {
        return <div className="tab-panels">{children}</div>;
      }
      
      function TabPanel({ children, index }: { children: ReactNode; index: number }) {
        const { activeIndex } = useTabsContext();
        if (activeIndex !== index) return null;
        return (
          <div role="tabpanel" className="tab-panel">
            {children}
          </div>
        );
      }
      
      // Attach as static properties
      Tabs.List = TabList;
      Tabs.Tab = Tab;
      Tabs.Panels = TabPanels;
      Tabs.Panel = TabPanel;
      
      // --- Usage ---
      function App() {
        return (
          <Tabs defaultIndex={0}>
            <Tabs.List>
              <Tabs.Tab index={0}>Profile</Tabs.Tab>
              <Tabs.Tab index={1}>Settings</Tabs.Tab>
            </Tabs.List>
            <Tabs.Panels>
              <Tabs.Panel index={0}>Profile content</Tabs.Panel>
              <Tabs.Panel index={1}>Settings content</Tabs.Panel>
            </Tabs.Panels>
          </Tabs>
        );
      }
      ```
      
      ---
      
      ## Render Props
      
      Render props delegate rendering to the consumer. Use for headless/unstyled component libraries where the logic is fixed but appearance varies.
      
      ```tsx
      import { useState, ReactNode } from 'react';
      
      interface ToggleRenderProps {
        on: boolean;
        toggle: () => void;
        setOn: (value: boolean) => void;
      }
      
      function Toggle({
        initial = false,
        children,
      }: {
        initial?: boolean;
        children: (props: ToggleRenderProps) => ReactNode;
      }) {
        const [on, setOn] = useState(initial);
        return <>{children({ on, toggle: () => setOn(v => !v), setOn })}</>;
      }
      
      // Usage: consumer controls rendering
      function DarkModeButton() {
        return (
          <Toggle>
            {({ on, toggle }) => (
              <button
                onClick={toggle}
                aria-label={on ? 'Switch to light mode' : 'Switch to dark mode'}
              >
                {on ? '🌙' : '☀️'}
              </button>
            )}
          </Toggle>
        );
      }
      
      // Prefer custom hooks over render props in modern React —
      // they achieve the same reuse with less JSX nesting
      function useToggle(initial = false) {
        const [on, setOn] = useState(initial);
        return { on, toggle: () => setOn(v => !v), setOn };
      }
      ```
      
      ---
      
      ## Higher-Order Components (HOC)
      
      HOCs wrap a component to inject props or add behavior. Prefer custom hooks for pure logic; use HOCs when you need to conditionally render or wrap JSX.
      
      ```tsx
      import { ComponentType, useEffect } from 'react';
      import { useNavigate } from 'react-router-dom';
      
      // --- Auth HOC ---
      interface WithAuthOptions {
        redirectTo?: string;
      }
      
      function withAuth<P extends object>(
        Component: ComponentType<P>,
        options: WithAuthOptions = {}
      ) {
        const { redirectTo = '/login' } = options;
      
        function AuthenticatedComponent(props: P) {
          const { user, isLoading } = useAuth();
          const navigate = useNavigate();
      
          useEffect(() => {
            if (!isLoading && !user) navigate(redirectTo);
          }, [user, isLoading, navigate]);
      
          if (isLoading) return <FullPageSpinner />;
          if (!user) return null;
      
          return <Component {...props} />;
        }
      
        // Preserve display name for DevTools
        AuthenticatedComponent.displayName = `withAuth(${Component.displayName ?? Component.name})`;
        return AuthenticatedComponent;
      }
      
      // Usage
      const ProtectedDashboard = withAuth(Dashboard);
      const AdminPanel = withAuth(AdminDashboard, { redirectTo: '/unauthorized' });
      
      // --- Logging HOC ---
      function withLogging<P extends object>(
        Component: ComponentType<P>,
        componentName: string
      ) {
        function LoggedComponent(props: P) {
          useEffect(() => {
            console.log(`[Mount] ${componentName}`);
            return () => console.log(`[Unmount] ${componentName}`);
          }, []);
      
          return <Component {...props} />;
        }
      
        LoggedComponent.displayName = `withLogging(${componentName})`;
        return LoggedComponent;
      }
      ```
      
      ---
      
      ## Controlled vs Uncontrolled Components
      
      ### Controlled
      
      ```tsx
      import { useState } from 'react';
      
      // Controlled: parent owns and controls the value
      function ControlledInput({
        value,
        onChange,
        label,
      }: {
        value: string;
        onChange: (value: string) => void;
        label: string;
      }) {
        return (
          <label>
            {label}
            <input
              type="text"
              value={value}
              onChange={e => onChange(e.target.value)}
            />
          </label>
        );
      }
      
      function Parent() {
        const [name, setName] = useState('');
        return <ControlledInput value={name} onChange={setName} label="Name" />;
      }
      ```
      
      ### Uncontrolled with Imperative Handle
      
      ```tsx
      import { forwardRef, useImperativeHandle, useRef, useState } from 'react';
      
      interface InputHandle {
        focus: () => void;
        clear: () => void;
        getValue: () => string;
      }
      
      // Hybrid: uncontrolled internally, but exposes imperative API via ref
      const SmartInput = forwardRef<InputHandle, { defaultValue?: string }>(
        function SmartInput({ defaultValue = '' }, ref) {
          const inputRef = useRef<HTMLInputElement>(null);
          const [value, setValue] = useState(defaultValue);
      
          useImperativeHandle(ref, () => ({
            focus: () => inputRef.current?.focus(),
            clear: () => setValue(''),
            getValue: () => value,
          }));
      
          return (
            <input
              ref={inputRef}
              value={value}
              onChange={e => setValue(e.target.value)}
            />
          );
        }
      );
      
      // Usage
      function Form() {
        const inputRef = useRef<InputHandle>(null);
      
        const handleSubmit = () => {
          const value = inputRef.current?.getValue();
          if (!value?.trim()) {
            inputRef.current?.focus();
            return;
          }
          submitForm(value);
          inputRef.current?.clear();
        };
      
        return (
          <>
            <SmartInput ref={inputRef} defaultValue="" />
            <button onClick={handleSubmit}>Submit</button>
          </>
        );
      }
      ```
      
      ---
      
      ## Error Boundaries
      
      Error boundaries must be class components. Use `react-error-boundary` package in production for less boilerplate.
      
      ```tsx
      import { Component, ErrorInfo, ReactNode } from 'react';
      
      interface Props {
        children: ReactNode;
        fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
        onError?: (error: Error, info: ErrorInfo) => void;
      }
      
      interface State {
        hasError: boolean;
        error: Error | null;
      }
      
      class ErrorBoundary extends Component<Props, State> {
        state: State = { hasError: false, error: null };
      
        static getDerivedStateFromError(error: Error): State {
          return { hasError: true, error };
        }
      
        componentDidCatch(error: Error, info: ErrorInfo) {
          // Log to error tracking service (Sentry, Datadog, etc.)
          this.props.onError?.(error, info);
          console.error('ErrorBoundary caught:', error, info.componentStack);
        }
      
        reset = () => this.setState({ hasError: false, error: null });
      
        render() {
          if (this.state.hasError && this.state.error) {
            const { fallback } = this.props;
            return typeof fallback === 'function'
              ? fallback(this.state.error, this.reset)
              : fallback;
          }
          return this.props.children;
        }
      }
      
      // Usage with error recovery
      function App() {
        return (
          <ErrorBoundary
            fallback={(error, reset) => (
              <div role="alert">
                <h2>Something went wrong</h2>
                <p>{error.message}</p>
                <button onClick={reset}>Try Again</button>
              </div>
            )}
            onError={(error) => Sentry.captureException(error)}
          >
            <Dashboard />
          </ErrorBoundary>
        );
      }
      
      // react-error-boundary package (recommended for production)
      import { ErrorBoundary } from 'react-error-boundary';
      
      function ErrorFallback({ error, resetErrorBoundary }: {
        error: Error;
        resetErrorBoundary: () => void;
      }) {
        return (
          <div role="alert">
            <p>{error.message}</p>
            <button onClick={resetErrorBoundary}>Retry</button>
          </div>
        );
      }
      
      <ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => queryClient.clear()}>
        <App />
      </ErrorBoundary>
      ```
      
      ---
      
      ## Portals
      
      Portals render children into a DOM node outside the current React tree. Useful for modals, tooltips, and toasts that need to escape overflow/z-index constraints.
      
      ```tsx
      import { createPortal } from 'react-dom';
      import { useEffect, useRef, ReactNode } from 'react';
      
      function Modal({
        isOpen,
        onClose,
        children,
        title,
      }: {
        isOpen: boolean;
        onClose: () => void;
        children: ReactNode;
        title: string;
      }) {
        const dialogRef = useRef<HTMLDialogElement>(null);
      
        // Trap focus and handle Escape key
        useEffect(() => {
          const dialog = dialogRef.current;
          if (!dialog) return;
      
          if (isOpen) {
            dialog.showModal();
          } else {
            dialog.close();
          }
        }, [isOpen]);
      
        if (!isOpen) return null;
      
        // Renders outside current DOM tree, into document.body
        return createPortal(
          <dialog
            ref={dialogRef}
            aria-labelledby="modal-title"
            aria-modal="true"
            onClose={onClose}
          >
            <h2 id="modal-title">{title}</h2>
            <div>{children}</div>
            <button onClick={onClose} aria-label="Close modal">
              &times;
            </button>
          </dialog>,
          document.body
        );
      }
      ```
      
      ---
      
      ## forwardRef
      
      ```tsx
      import { forwardRef, InputHTMLAttributes } from 'react';
      
      interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
        label: string;
        error?: string;
      }
      
      // React 19: ref is now a regular prop, forwardRef not required
      // For React 18 and below:
      const Input = forwardRef<HTMLInputElement, InputProps>(
        function Input({ label, error, id, ...props }, ref) {
          const inputId = id ?? label.toLowerCase().replace(/\s+/g, '-');
      
          return (
            <div className="input-wrapper">
              <label htmlFor={inputId}>{label}</label>
              <input
                ref={ref}
                id={inputId}
                aria-describedby={error ? `${inputId}-error` : undefined}
                aria-invalid={!!error}
                {...props}
              />
              {error && (
                <span id={`${inputId}-error`} role="alert" className="error">
                  {error}
                </span>
              )}
            </div>
          );
        }
      );
      
      Input.displayName = 'Input';
      
      // React 19 equivalent (no forwardRef needed):
      function InputV19({ label, ref, error, id, ...props }: InputProps & {
        ref?: React.Ref<HTMLInputElement>;
      }) {
        const inputId = id ?? label.toLowerCase().replace(/\s+/g, '-');
        return (
          <div>
            <label htmlFor={inputId}>{label}</label>
            <input ref={ref} id={inputId} {...props} />
          </div>
        );
      }
      ```
      
      ---
      
      ## Slot Pattern
      
      Named slots via props allow flexible composition without rigid component trees.
      
      ```tsx
      import { ReactNode } from 'react';
      
      interface CardProps {
        header: ReactNode;
        children: ReactNode;
        footer?: ReactNode;
        aside?: ReactNode;
      }
      
      function Card({ header, children, footer, aside }: CardProps) {
        return (
          <div className="card">
            <div className="card__header">{header}</div>
            <div className="card__body">
              <div className="card__content">{children}</div>
              {aside && <aside className="card__aside">{aside}</aside>}
            </div>
            {footer && <footer className="card__footer">{footer}</footer>}
          </div>
        );
      }
      
      // Usage: consumer fills each slot independently
      function ProductCard({ product }: { product: Product }) {
        return (
          <Card
            header={<img src={product.image} alt={product.name} />}
            footer={<AddToCartButton productId={product.id} />}
            aside={<ProductRating rating={product.rating} />}
          >
            <h3>{product.name}</h3>
            <p>{product.description}</p>
          </Card>
        );
      }
      ```
      
      ---
      
      ## Polymorphic Components (as prop)
      
      ```tsx
      import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react';
      
      // Generic polymorphic component type
      type PolymorphicProps<C extends ElementType, P = object> = {
        as?: C;
        children?: ReactNode;
      } & P &
        Omit<ComponentPropsWithoutRef<C>, keyof P | 'as' | 'children'>;
      
      // Button that can render as <button>, <a>, or any element
      function Button<C extends ElementType = 'button'>({
        as,
        children,
        variant = 'primary',
        ...props
      }: PolymorphicProps<C, { variant?: 'primary' | 'secondary' | 'ghost' }>) {
        const Component = as ?? 'button';
        return (
          <Component className={`btn btn--${variant}`} {...props}>
            {children}
          </Component>
        );
      }
      
      // Usage — TypeScript infers correct HTML attributes
      <Button onClick={() => {}}>Click me</Button>             // renders <button>
      <Button as="a" href="/about">About</Button>               // renders <a>, href is valid
      <Button as="a" href="/about" variant="secondary">Link</Button>
      ```
      
      ---
      
      ## Container / Presentational Split
      
      Largely superseded by hooks, but useful when separating data-fetching from display for testing.
      
      ```tsx
      // Presentational: receives data as props, no fetching
      function UserListView({
        users,
        isLoading,
        error,
        onDelete,
      }: {
        users: User[];
        isLoading: boolean;
        error: Error | null;
        onDelete: (id: string) => void;
      }) {
        if (isLoading) return <Spinner />;
        if (error) return <ErrorMessage error={error} />;
        return (
          <ul>
            {users.map(user => (
              <li key={user.id}>
                {user.name}
                <button onClick={() => onDelete(user.id)}>Delete</button>
              </li>
            ))}
          </ul>
        );
      }
      
      // Container: owns data-fetching, passes to presentational
      function UserListContainer() {
        const { data: users = [], isLoading, error } = useQuery(['users'], fetchUsers);
        const deleteMutation = useMutation(deleteUser, {
          onSuccess: () => queryClient.invalidateQueries(['users']),
        });
      
        return (
          <UserListView
            users={users}
            isLoading={isLoading}
            error={error ?? null}
            onDelete={id => deleteMutation.mutate(id)}
          />
        );
      }
      ```
      
      ---
      
      ## Patterns to Avoid
      
      | Anti-pattern | Problem | Fix |
      |--------------|---------|-----|
      | Prop drilling past 3 levels | Hard to maintain, tightly coupled | Compound components or Context |
      | HOC for pure logic (no JSX needed) | Creates wrapper component unnecessarily | Custom hook instead |
      | Huge single component (500+ lines) | Hard to test, reuse, understand | Split by responsibility |
      | `any` in component props | Loses type safety | Type all props; use `unknown` with narrowing |
      | `key` on React.Fragment without need | Unnecessary | Only add key when rendering lists |
      | Mutable props | Breaks React's unidirectional data flow | Lift state or use callback |
      | Boolean props without clear intent | `<Input disabled />` vs `<Input disabled={false}>` | Always explicit: `disabled={isLoading}` |
      
    • hooks-patterns.md 20.1 KB
      # Hooks Patterns
      
      Deep reference for React hooks — built-in hooks, custom hook recipes, React 19 hooks, and composition patterns.
      
      ---
      
      ## useState
      
      ### Initializer Function (Lazy Initial State)
      
      When initial state is expensive to compute, pass a function — it runs only once.
      
      ```typescript
      import { useState } from 'react';
      
      // BAD: parseExpensiveData runs on every render
      const [data, setData] = useState(parseExpensiveData(rawInput));
      
      // GOOD: runs once at mount
      const [data, setData] = useState(() => parseExpensiveData(rawInput));
      
      // GOOD: reading from localStorage (sync, only once)
      const [theme, setTheme] = useState<'light' | 'dark'>(
        () => (localStorage.getItem('theme') as 'light' | 'dark') ?? 'light'
      );
      ```
      
      ### Functional Updates
      
      When new state depends on previous state, always use the functional form to avoid stale closures.
      
      ```typescript
      function Counter() {
        const [count, setCount] = useState(0);
      
        // BAD: if called rapidly, `count` might be stale
        const increment = () => setCount(count + 1);
      
        // GOOD: always receives the latest state
        const increment = () => setCount(prev => prev + 1);
      
        // GOOD: batch multiple updates
        const incrementBy3 = () => {
          setCount(prev => prev + 1);
          setCount(prev => prev + 1);
          setCount(prev => prev + 1);
        };
      
        return <button onClick={increment}>{count}</button>;
      }
      ```
      
      ### Object State
      
      ```typescript
      interface FormState {
        name: string;
        email: string;
        age: number;
      }
      
      function ProfileForm() {
        const [form, setForm] = useState<FormState>({
          name: '',
          email: '',
          age: 0,
        });
      
        // Partial update pattern — spread to preserve other fields
        const updateField = <K extends keyof FormState>(
          key: K,
          value: FormState[K]
        ) => setForm(prev => ({ ...prev, [key]: value }));
      
        return (
          <input
            value={form.name}
            onChange={e => updateField('name', e.target.value)}
          />
        );
      }
      ```
      
      ---
      
      ## useReducer
      
      Use when state transitions are complex, involve multiple sub-values, or next state depends on previous in non-trivial ways.
      
      ```typescript
      import { useReducer } from 'react';
      
      // 1. Define state shape
      interface CartState {
        items: CartItem[];
        total: number;
        isCheckingOut: boolean;
      }
      
      // 2. Define discriminated union of actions
      type CartAction =
        | { type: 'ADD_ITEM'; payload: CartItem }
        | { type: 'REMOVE_ITEM'; payload: { id: string } }
        | { type: 'CLEAR_CART' }
        | { type: 'SET_CHECKOUT'; payload: boolean };
      
      // 3. Reducer — pure function, no side effects
      function cartReducer(state: CartState, action: CartAction): CartState {
        switch (action.type) {
          case 'ADD_ITEM':
            return {
              ...state,
              items: [...state.items, action.payload],
              total: state.total + action.payload.price,
            };
          case 'REMOVE_ITEM': {
            const removed = state.items.find(i => i.id === action.payload.id);
            return {
              ...state,
              items: state.items.filter(i => i.id !== action.payload.id),
              total: state.total - (removed?.price ?? 0),
            };
          }
          case 'CLEAR_CART':
            return { items: [], total: 0, isCheckingOut: false };
          case 'SET_CHECKOUT':
            return { ...state, isCheckingOut: action.payload };
          default:
            // TypeScript exhaustiveness check
            action satisfies never;
            return state;
        }
      }
      
      const initialState: CartState = { items: [], total: 0, isCheckingOut: false };
      
      function Cart() {
        const [state, dispatch] = useReducer(cartReducer, initialState);
      
        return (
          <div>
            <p>Items: {state.items.length}</p>
            <p>Total: ${state.total}</p>
            <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>Clear</button>
          </div>
        );
      }
      ```
      
      ---
      
      ## useRef
      
      ### DOM Access
      
      ```typescript
      import { useRef, useEffect } from 'react';
      
      function AutoFocusInput() {
        const inputRef = useRef<HTMLInputElement>(null);
      
        useEffect(() => {
          // ref.current is the DOM node after mount
          inputRef.current?.focus();
        }, []);
      
        return <input ref={inputRef} placeholder="Auto-focused" />;
      }
      ```
      
      ### Mutable Value (No Re-render)
      
      ```typescript
      function Stopwatch() {
        const [elapsed, setElapsed] = useState(0);
        // Store timer ID without triggering re-renders
        const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
      
        const start = () => {
          if (intervalRef.current !== null) return;
          intervalRef.current = setInterval(() => {
            setElapsed(prev => prev + 1);
          }, 1000);
        };
      
        const stop = () => {
          if (intervalRef.current === null) return;
          clearInterval(intervalRef.current);
          intervalRef.current = null;
        };
      
        // Clean up on unmount
        useEffect(() => () => stop(), []);
      
        return (
          <div>
            <p>{elapsed}s</p>
            <button onClick={start}>Start</button>
            <button onClick={stop}>Stop</button>
          </div>
        );
      }
      ```
      
      ---
      
      ## useEffect
      
      ### Cleanup Pattern
      
      Every subscription, timer, or fetch should have a cleanup.
      
      ```typescript
      import { useEffect, useState } from 'react';
      
      // Pattern: subscription with cleanup
      function useWindowSize() {
        const [size, setSize] = useState({
          width: window.innerWidth,
          height: window.innerHeight,
        });
      
        useEffect(() => {
          const handler = () => {
            setSize({ width: window.innerWidth, height: window.innerHeight });
          };
      
          window.addEventListener('resize', handler);
      
          // Cleanup removes listener — runs before next effect and on unmount
          return () => window.removeEventListener('resize', handler);
        }, []); // empty array = run once at mount
      
        return size;
      }
      ```
      
      ### Async in useEffect
      
      ```typescript
      useEffect(() => {
        // WRONG: async function returns Promise, not cleanup
        // useEffect(async () => { ... }, []);
      
        // CORRECT: define async function, call it immediately
        const controller = new AbortController();
      
        async function fetchData() {
          try {
            const res = await fetch(`/api/users/${userId}`, {
              signal: controller.signal,
            });
            const data = await res.json();
            setUser(data);
          } catch (err) {
            if (err instanceof Error && err.name !== 'AbortError') {
              setError(err);
            }
          }
        }
      
        fetchData();
      
        // Abort in-flight request if userId changes or component unmounts
        return () => controller.abort();
      }, [userId]);
      ```
      
      ### useLayoutEffect vs useEffect
      
      ```typescript
      import { useLayoutEffect, useEffect, useRef } from 'react';
      
      // useLayoutEffect: fires synchronously AFTER DOM mutations, BEFORE paint
      // Use for: measuring DOM, preventing visual flicker
      function Tooltip({ anchorRef }: { anchorRef: React.RefObject<HTMLElement> }) {
        const tooltipRef = useRef<HTMLDivElement>(null);
      
        useLayoutEffect(() => {
          // Measure anchor position and position tooltip BEFORE browser paints
          const anchor = anchorRef.current;
          const tooltip = tooltipRef.current;
          if (!anchor || !tooltip) return;
      
          const rect = anchor.getBoundingClientRect();
          tooltip.style.top = `${rect.bottom + 8}px`;
          tooltip.style.left = `${rect.left}px`;
        });
      
        return <div ref={tooltipRef} className="tooltip">Tooltip</div>;
      }
      
      // useEffect: fires asynchronously AFTER paint
      // Use for: data fetching, subscriptions, analytics — anything that doesn't
      // need to block the browser paint
      ```
      
      ---
      
      ## Custom Hooks
      
      ### useFetch with AbortController
      
      ```typescript
      import { useState, useEffect, useCallback } from 'react';
      
      interface FetchState<T> {
        data: T | null;
        error: Error | null;
        isLoading: boolean;
      }
      
      function useFetch<T>(url: string) {
        const [state, setState] = useState<FetchState<T>>({
          data: null,
          error: null,
          isLoading: true,
        });
      
        const refetch = useCallback(() => {
          const controller = new AbortController();
          setState(prev => ({ ...prev, isLoading: true, error: null }));
      
          fetch(url, { signal: controller.signal })
            .then(res => {
              if (!res.ok) throw new Error(`HTTP ${res.status}`);
              return res.json() as Promise<T>;
            })
            .then(data => setState({ data, error: null, isLoading: false }))
            .catch(err => {
              if (err.name !== 'AbortError') {
                setState({ data: null, error: err, isLoading: false });
              }
            });
      
          return () => controller.abort();
        }, [url]);
      
        useEffect(() => {
          const cleanup = refetch();
          return cleanup;
        }, [refetch]);
      
        return { ...state, refetch };
      }
      
      // Usage
      function UserProfile({ id }: { id: string }) {
        const { data, error, isLoading, refetch } = useFetch<User>(`/api/users/${id}`);
      
        if (isLoading) return <Spinner />;
        if (error) return <Error message={error.message} onRetry={refetch} />;
        return <div>{data?.name}</div>;
      }
      ```
      
      ### useLocalStorage (SSR-safe)
      
      ```typescript
      import { useState, useEffect, useCallback } from 'react';
      
      function useLocalStorage<T>(key: string, initialValue: T) {
        // Read from localStorage with SSR safety
        const readValue = useCallback((): T => {
          if (typeof window === 'undefined') return initialValue;
          try {
            const item = window.localStorage.getItem(key);
            return item ? (JSON.parse(item) as T) : initialValue;
          } catch {
            console.warn(`Error reading localStorage key "${key}"`);
            return initialValue;
          }
        }, [key, initialValue]);
      
        const [storedValue, setStoredValue] = useState<T>(readValue);
      
        const setValue = useCallback(
          (value: T | ((val: T) => T)) => {
            try {
              const valueToStore =
                value instanceof Function ? value(storedValue) : value;
              setStoredValue(valueToStore);
              if (typeof window !== 'undefined') {
                window.localStorage.setItem(key, JSON.stringify(valueToStore));
              }
            } catch {
              console.warn(`Error setting localStorage key "${key}"`);
            }
          },
          [key, storedValue]
        );
      
        // Sync across tabs
        useEffect(() => {
          const handleStorageChange = (event: StorageEvent) => {
            if (event.key === key) {
              setStoredValue(readValue());
            }
          };
          window.addEventListener('storage', handleStorageChange);
          return () => window.removeEventListener('storage', handleStorageChange);
        }, [key, readValue]);
      
        return [storedValue, setValue] as const;
      }
      ```
      
      ### useDebounce
      
      ```typescript
      import { useState, useEffect } from 'react';
      
      function useDebounce<T>(value: T, delay: number): T {
        const [debouncedValue, setDebouncedValue] = useState<T>(value);
      
        useEffect(() => {
          const timer = setTimeout(() => setDebouncedValue(value), delay);
          return () => clearTimeout(timer);
        }, [value, delay]);
      
        return debouncedValue;
      }
      
      // Usage: debounce search input before firing API call
      function SearchBar() {
        const [query, setQuery] = useState('');
        const debouncedQuery = useDebounce(query, 300);
      
        useEffect(() => {
          if (debouncedQuery) {
            searchApi(debouncedQuery);
          }
        }, [debouncedQuery]);
      
        return (
          <input
            value={query}
            onChange={e => setQuery(e.target.value)}
            placeholder="Search..."
          />
        );
      }
      ```
      
      ### useMediaQuery
      
      ```typescript
      import { useState, useEffect } from 'react';
      
      function useMediaQuery(query: string): boolean {
        const [matches, setMatches] = useState<boolean>(() => {
          if (typeof window === 'undefined') return false;
          return window.matchMedia(query).matches;
        });
      
        useEffect(() => {
          if (typeof window === 'undefined') return;
          const mql = window.matchMedia(query);
          const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
      
          // Use addEventListener (deprecated addListener removed in modern browsers)
          mql.addEventListener('change', handler);
          return () => mql.removeEventListener('change', handler);
        }, [query]);
      
        return matches;
      }
      
      // Predefined breakpoints matching Tailwind defaults
      export const useIsTablet = () => useMediaQuery('(min-width: 768px)');
      export const useIsDesktop = () => useMediaQuery('(min-width: 1024px)');
      export const usePrefersDark = () => useMediaQuery('(prefers-color-scheme: dark)');
      export const usePrefersReducedMotion = () =>
        useMediaQuery('(prefers-reduced-motion: reduce)');
      ```
      
      ### useIntersectionObserver
      
      ```typescript
      import { useEffect, useRef, useState } from 'react';
      
      interface UseIntersectionOptions extends IntersectionObserverInit {
        freezeOnceVisible?: boolean;
      }
      
      function useIntersectionObserver(options: UseIntersectionOptions = {}) {
        const { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false } = options;
        const elementRef = useRef<HTMLElement>(null);
        const [entry, setEntry] = useState<IntersectionObserverEntry | null>(null);
      
        const frozen = entry?.isIntersecting && freezeOnceVisible;
      
        useEffect(() => {
          const element = elementRef.current;
          if (!element || frozen) return;
      
          const observer = new IntersectionObserver(
            ([entry]) => setEntry(entry),
            { threshold, root, rootMargin }
          );
      
          observer.observe(element);
          return () => observer.disconnect();
        }, [threshold, root, rootMargin, frozen]);
      
        return { ref: elementRef, entry, isIntersecting: !!entry?.isIntersecting };
      }
      
      // Usage: lazy load images
      function LazyImage({ src, alt }: { src: string; alt: string }) {
        const { ref, isIntersecting } = useIntersectionObserver({
          threshold: 0.1,
          freezeOnceVisible: true,
        });
      
        return (
          <div ref={ref as React.RefObject<HTMLDivElement>} style={{ minHeight: 200 }}>
            {isIntersecting && <img src={src} alt={alt} loading="lazy" />}
          </div>
        );
      }
      ```
      
      ### usePrevious
      
      ```typescript
      import { useRef, useEffect } from 'react';
      
      function usePrevious<T>(value: T): T | undefined {
        const ref = useRef<T | undefined>(undefined);
      
        // Runs after render — ref holds value from previous render
        useEffect(() => {
          ref.current = value;
        }, [value]);
      
        // Returns value from before this render
        return ref.current;
      }
      
      // Usage: animate on value change
      function AnimatedCounter({ count }: { count: number }) {
        const prevCount = usePrevious(count);
        const direction = prevCount !== undefined && count > prevCount ? 'up' : 'down';
      
        return (
          <span className={`animate-${direction}`}>
            {count}
          </span>
        );
      }
      ```
      
      ### useEventListener
      
      ```typescript
      import { useEffect, useRef } from 'react';
      
      function useEventListener<K extends keyof WindowEventMap>(
        eventType: K,
        handler: (event: WindowEventMap[K]) => void,
        element: EventTarget = window
      ): void {
        // Use ref so handler changes don't cause re-subscription
        const handlerRef = useRef(handler);
        useEffect(() => { handlerRef.current = handler; });
      
        useEffect(() => {
          const listener = (event: Event) =>
            handlerRef.current(event as WindowEventMap[K]);
          element.addEventListener(eventType, listener);
          return () => element.removeEventListener(eventType, listener);
        }, [eventType, element]);
      }
      
      // Usage
      function KeyboardShortcut() {
        useEventListener('keydown', event => {
          if (event.key === 'Escape') closeModal();
          if ((event.metaKey || event.ctrlKey) && event.key === 'k') openSearch();
        });
      }
      ```
      
      ---
      
      ## Hook Composition
      
      Build complex hooks by composing simpler ones. Each hook should do one thing well.
      
      ```typescript
      // Compose useFetch + useDebounce for a search hook
      function useSearch<T>(endpoint: string) {
        const [query, setQuery] = useState('');
        const debouncedQuery = useDebounce(query, 300);
      
        // Only fetch when query is non-empty
        const url = debouncedQuery ? `${endpoint}?q=${encodeURIComponent(debouncedQuery)}` : null;
        const { data, isLoading, error } = useFetch<T[]>(url ?? '');
      
        return {
          query,
          setQuery,
          results: data ?? [],
          isLoading: isLoading && !!debouncedQuery,
          error,
        };
      }
      
      // Compose local storage + media query for responsive theme
      function useTheme() {
        const prefersDark = useMediaQuery('(prefers-color-scheme: dark)');
        const [savedTheme, setSavedTheme] = useLocalStorage<'light' | 'dark' | 'system'>(
          'theme',
          'system'
        );
      
        const resolvedTheme: 'light' | 'dark' =
          savedTheme === 'system' ? (prefersDark ? 'dark' : 'light') : savedTheme;
      
        return { theme: resolvedTheme, savedTheme, setTheme: setSavedTheme };
      }
      ```
      
      ---
      
      ## Rules of Hooks
      
      Only call hooks at the top level of a React function component or another custom hook. Never inside conditions, loops, or nested functions.
      
      ```typescript
      // VIOLATION: conditional hook call
      function BadComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
        if (isLoggedIn) {
          const user = useUser(); // ERROR: conditional
        }
      }
      
      // FIX: always call hooks, conditionally use their values
      function GoodComponent({ isLoggedIn }: { isLoggedIn: boolean }) {
        const user = useUser();
        if (!isLoggedIn) return null;
        return <div>{user.name}</div>;
      }
      
      // VIOLATION: hook in a loop
      function BadList({ ids }: { ids: string[] }) {
        return ids.map(id => {
          const data = useFetch(`/api/${id}`); // ERROR: in loop
          return <Item key={id} data={data} />;
        });
      }
      
      // FIX: move hook logic into a child component
      function GoodList({ ids }: { ids: string[] }) {
        return ids.map(id => <ListItem key={id} id={id} />);
      }
      
      function ListItem({ id }: { id: string }) {
        const data = useFetch(`/api/${id}`); // CORRECT: top level
        return <Item data={data} />;
      }
      ```
      
      ---
      
      ## React 19 Hooks
      
      ### use() — Promises and Context
      
      ```typescript
      import { use, Suspense } from 'react';
      
      // Await a promise directly in render (must be wrapped in Suspense)
      async function fetchUser(id: string): Promise<User> {
        const res = await fetch(`/api/users/${id}`);
        return res.json();
      }
      
      function UserCard({ userPromise }: { userPromise: Promise<User> }) {
        // Suspends until promise resolves; throws on rejection (ErrorBoundary handles it)
        const user = use(userPromise);
        return <div>{user.name}</div>;
      }
      
      function Page({ id }: { id: string }) {
        const userPromise = fetchUser(id); // start fetch, pass promise down
      
        return (
          <Suspense fallback={<Skeleton />}>
            <UserCard userPromise={userPromise} />
          </Suspense>
        );
      }
      
      // use() can also read context conditionally (unlike useContext)
      function ConditionalTheme({ showLabel }: { showLabel: boolean }) {
        if (!showLabel) return null;
        const theme = use(ThemeContext); // conditional — allowed with use()
        return <span style={{ color: theme.primary }}>Label</span>;
      }
      ```
      
      ### useFormStatus
      
      ```typescript
      import { useFormStatus } from 'react-dom';
      
      // Must be used inside a <form> with an action
      function SubmitButton() {
        const { pending, data, method } = useFormStatus();
        return (
          <button type="submit" disabled={pending}>
            {pending ? 'Saving...' : 'Save'}
          </button>
        );
      }
      
      function ProfileForm() {
        return (
          <form action={updateProfileAction}>
            <input name="bio" />
            <SubmitButton /> {/* useFormStatus works here */}
          </form>
        );
      }
      ```
      
      ### useOptimistic
      
      ```typescript
      import { useOptimistic, useTransition } from 'react';
      
      interface Message {
        id: string;
        text: string;
        sending?: boolean;
      }
      
      function MessageList({ messages }: { messages: Message[] }) {
        const [optimisticMessages, addOptimisticMessage] = useOptimistic(
          messages,
          // Reducer: how to merge optimistic update into current state
          (currentMessages, newMessage: Message) => [
            ...currentMessages,
            { ...newMessage, sending: true },
          ]
        );
      
        async function sendMessage(formData: FormData) {
          const text = formData.get('text') as string;
          const tempMessage = { id: crypto.randomUUID(), text };
      
          // Update UI immediately
          addOptimisticMessage(tempMessage);
      
          // Send to server (optimistic update reverts on error)
          await saveMessage(text);
        }
      
        return (
          <>
            {optimisticMessages.map(msg => (
              <div key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
                {msg.text}
              </div>
            ))}
            <form action={sendMessage}>
              <input name="text" />
              <button type="submit">Send</button>
            </form>
          </>
        );
      }
      ```
      
      ---
      
      ## Anti-patterns
      
      | Anti-pattern | Problem | Fix |
      |--------------|---------|-----|
      | `useEffect` with no dep array syncing props to state | Runs every render | Compute derived value during render |
      | Calling hooks from event handlers | Violates rules of hooks | Move hook to component top level |
      | `useState` for server data | Manual loading/error state, stale data | Use TanStack Query |
      | Large single `useEffect` doing multiple things | Hard to reason about, wrong deps | Split into separate `useEffect` calls per concern |
      | `useCallback` on everything | Adds overhead, no benefit without memoized children | Only when callback is a dep or passed to `memo` component |
      | Forgetting cleanup | Memory leaks, stale updates on unmounted component | Always return cleanup from `useEffect` |
      
    • performance.md 17.6 KB
      # Performance
      
      React performance patterns: memoization, code splitting, virtualization, React Compiler, profiling, and Web Vitals.
      
      ---
      
      ## Memoization
      
      ### React.memo
      
      Skips re-render when props haven't changed (shallow equality by default).
      
      ```tsx
      import { memo, useCallback, useState } from 'react';
      
      interface ListItemProps {
        item: { id: string; name: string; count: number };
        onDelete: (id: string) => void;
      }
      
      // Memoize expensive list items so parent re-renders don't cascade
      const ListItem = memo(function ListItem({ item, onDelete }: ListItemProps) {
        console.log(`Rendering ${item.name}`); // only logs when item or onDelete changes
        return (
          <li>
            {item.name} ({item.count})
            <button onClick={() => onDelete(item.id)}>Delete</button>
          </li>
        );
      });
      
      // Custom comparison — return true to SKIP re-render
      const ExpensiveChart = memo(
        function ExpensiveChart({ data, config }: ChartProps) {
          return <Canvas data={data} config={config} />;
        },
        (prevProps, nextProps) => {
          // Only re-render if data length changes or config changes
          return (
            prevProps.data.length === nextProps.data.length &&
            prevProps.config.type === nextProps.config.type
          );
        }
      );
      
      // Parent must stabilize callbacks with useCallback to benefit from memo
      function ItemList({ items }: { items: Item[] }) {
        const [filter, setFilter] = useState('');
      
        // Without useCallback, new function reference every render → memo is useless
        const handleDelete = useCallback((id: string) => {
          deleteItem(id);
        }, []); // stable — no deps
      
        return (
          <ul>
            {items.map(item => (
              <ListItem key={item.id} item={item} onDelete={handleDelete} />
            ))}
          </ul>
        );
      }
      ```
      
      ### When NOT to Use React.memo
      
      ```tsx
      // BAD: memo on a component that almost always re-renders anyway
      const SimpleDiv = memo(({ children }: { children: React.ReactNode }) => (
        <div>{children}</div>
      ));
      
      // BAD: memo where props contain new objects/arrays every render
      function Parent() {
        return (
          // options is a new array every render — memo never skips
          <MemoizedChild options={['a', 'b', 'c']} />
        );
      }
      
      // GOOD: only memo when:
      // 1. Component renders the same output given the same props
      // 2. Re-renders frequently with same props (large lists, heavy computation)
      // 3. Props are primitives or stable references
      ```
      
      ### useMemo
      
      ```tsx
      import { useMemo, useState } from 'react';
      
      function ProductList({ products }: { products: Product[] }) {
        const [sortBy, setSortBy] = useState<'price' | 'name'>('name');
        const [filter, setFilter] = useState('');
      
        // Expensive: filter + sort on every render without memoization
        const processedProducts = useMemo(() => {
          const filtered = products.filter(p =>
            p.name.toLowerCase().includes(filter.toLowerCase())
          );
          return filtered.sort((a, b) =>
            sortBy === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
          );
        }, [products, filter, sortBy]); // only recalculates when these change
      
        return (
          <ul>
            {processedProducts.map(p => <ProductCard key={p.id} product={p} />)}
          </ul>
        );
      }
      
      // When NOT to use useMemo
      function BadUsage() {
        // BAD: simple operations don't need memoization — the overhead costs more
        const doubled = useMemo(() => count * 2, [count]);
        const greeting = useMemo(() => `Hello, ${name}`, [name]);
      
        // GOOD: compute inline
        const doubled = count * 2;
        const greeting = `Hello, ${name}`;
      }
      ```
      
      ### useCallback
      
      ```tsx
      import { useCallback, useState, memo } from 'react';
      
      // useCallback returns a stable function reference
      // Only useful when passed to: memo() components, useEffect dep arrays, other callbacks
      
      function SearchPage() {
        const [query, setQuery] = useState('');
        const [results, setResults] = useState<Result[]>([]);
      
        // Stable reference: won't cause SearchResults to re-render when SearchPage renders
        const handleResultClick = useCallback((id: string) => {
          trackClick(id); // does not depend on any state
        }, []);
      
        // Correct deps: includeArchived is used inside the callback
        const [includeArchived, setIncludeArchived] = useState(false);
        const search = useCallback(async (q: string) => {
          const data = await fetchResults(q, { includeArchived });
          setResults(data);
        }, [includeArchived]); // re-created when includeArchived changes
      
        return (
          <>
            <SearchInput value={query} onChange={setQuery} onSearch={search} />
            <MemoizedResults results={results} onResultClick={handleResultClick} />
          </>
        );
      }
      ```
      
      ---
      
      ## Code Splitting
      
      ### React.lazy + Suspense
      
      ```tsx
      import { lazy, Suspense, useState } from 'react';
      
      // Dynamic import — loaded only when rendered
      const HeavyEditor = lazy(() => import('./HeavyEditor'));
      const DataVizChart = lazy(() => import('./DataVizChart'));
      
      // Preload on hover for instant perceived load
      function preloadEditor() {
        const promise = import('./HeavyEditor');
        return promise;
      }
      
      function Dashboard() {
        const [showEditor, setShowEditor] = useState(false);
      
        return (
          <div>
            <button
              onClick={() => setShowEditor(true)}
              onMouseEnter={preloadEditor} // start loading before click
            >
              Open Editor
            </button>
      
            {showEditor && (
              <Suspense fallback={<EditorSkeleton />}>
                <HeavyEditor />
              </Suspense>
            )}
      
            <Suspense fallback={<ChartSkeleton />}>
              <DataVizChart />
            </Suspense>
          </div>
        );
      }
      ```
      
      ### Route-Based Splitting (React Router)
      
      ```tsx
      import { lazy, Suspense } from 'react';
      import { Routes, Route } from 'react-router-dom';
      
      // Each route is its own chunk
      const HomePage = lazy(() => import('./pages/Home'));
      const DashboardPage = lazy(() => import('./pages/Dashboard'));
      const SettingsPage = lazy(() => import('./pages/Settings'));
      
      function App() {
        return (
          <Suspense fallback={<PageLoader />}>
            <Routes>
              <Route path="/" element={<HomePage />} />
              <Route path="/dashboard" element={<DashboardPage />} />
              <Route path="/settings" element={<SettingsPage />} />
            </Routes>
          </Suspense>
        );
      }
      ```
      
      ---
      
      ## Avoiding Re-renders
      
      ### State Colocation
      
      ```tsx
      // BAD: state in parent causes all children to re-render
      function Parent() {
        const [inputValue, setInputValue] = useState('');
        return (
          <>
            <input value={inputValue} onChange={e => setInputValue(e.target.value)} />
            <ExpensiveComponent /> {/* re-renders on every keystroke! */}
            <AnotherExpensiveComponent />
          </>
        );
      }
      
      // GOOD: colocate state where it's needed
      function InputSection() {
        const [inputValue, setInputValue] = useState('');
        return <input value={inputValue} onChange={e => setInputValue(e.target.value)} />;
      }
      
      function Parent() {
        return (
          <>
            <InputSection />       {/* only this re-renders */}
            <ExpensiveComponent />  {/* never re-renders */}
            <AnotherExpensiveComponent />
          </>
        );
      }
      ```
      
      ### Children Pattern
      
      ```tsx
      // BAD: wrapping component re-renders on every parent render
      function Wrapper() {
        const [count, setCount] = useState(0);
        return (
          <div>
            <button onClick={() => setCount(c => c + 1)}>{count}</button>
            <SlowComponent />  {/* re-renders even though it doesn't use count */}
          </div>
        );
      }
      
      // GOOD: pass slow component as children — it's created in parent, not re-rendered
      function WrapperWithChildren({ children }: { children: React.ReactNode }) {
        const [count, setCount] = useState(0);
        return (
          <div>
            <button onClick={() => setCount(c => c + 1)}>{count}</button>
            {children} {/* reference is stable, SlowComponent doesn't re-render */}
          </div>
        );
      }
      
      function App() {
        return (
          <WrapperWithChildren>
            <SlowComponent />
          </WrapperWithChildren>
        );
      }
      ```
      
      ---
      
      ## Concurrent Features
      
      ### useTransition
      
      ```tsx
      import { useState, useTransition } from 'react';
      
      function FilterableList({ items }: { items: Item[] }) {
        const [filter, setFilter] = useState('');
        const [filteredItems, setFilteredItems] = useState(items);
        const [isPending, startTransition] = useTransition();
      
        const handleFilterChange = (value: string) => {
          // Urgent: update input immediately
          setFilter(value);
      
          // Non-urgent: defer the expensive filtering
          startTransition(() => {
            const filtered = items.filter(item =>
              item.name.toLowerCase().includes(value.toLowerCase())
            );
            setFilteredItems(filtered);
          });
        };
      
        return (
          <>
            <input
              value={filter}
              onChange={e => handleFilterChange(e.target.value)}
              placeholder="Filter..."
            />
            {/* Show stale content with opacity while pending */}
            <ul style={{ opacity: isPending ? 0.7 : 1 }}>
              {filteredItems.map(item => <li key={item.id}>{item.name}</li>)}
            </ul>
          </>
        );
      }
      ```
      
      ### useDeferredValue
      
      ```tsx
      import { useState, useDeferredValue, memo } from 'react';
      
      // useDeferredValue: defer a value derived from props/state
      // Unlike useTransition, works when you don't own the state setter
      
      function SearchResults({ query }: { query: string }) {
        // Defer the slow part — input stays responsive
        const deferredQuery = useDeferredValue(query);
      
        return (
          <div style={{ opacity: query !== deferredQuery ? 0.7 : 1 }}>
            <SlowResultsList query={deferredQuery} />
          </div>
        );
      }
      
      // Must be memoized for useDeferredValue to have effect
      const SlowResultsList = memo(function SlowResultsList({ query }: { query: string }) {
        // Expensive rendering — now deferred
        const results = heavySearch(query);
        return results.map(r => <Result key={r.id} result={r} />);
      });
      ```
      
      ---
      
      ## Virtualization
      
      For lists with more than 100 items, only render what's visible.
      
      ```tsx
      import { useVirtualizer } from '@tanstack/react-virtual';
      import { useRef } from 'react';
      
      function VirtualList({ items }: { items: Item[] }) {
        const parentRef = useRef<HTMLDivElement>(null);
      
        const virtualizer = useVirtualizer({
          count: items.length,
          getScrollElement: () => parentRef.current,
          estimateSize: () => 60, // estimated row height in px
          overscan: 5,            // render 5 extra items outside viewport
        });
      
        return (
          // Scrollable container — must have a fixed height
          <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
            {/* Total height spacer so scrollbar is sized correctly */}
            <div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
              {virtualizer.getVirtualItems().map(virtualItem => (
                <div
                  key={virtualItem.key}
                  style={{
                    position: 'absolute',
                    top: 0,
                    left: 0,
                    width: '100%',
                    height: `${virtualItem.size}px`,
                    transform: `translateY(${virtualItem.start}px)`,
                  }}
                >
                  <ListItem item={items[virtualItem.index]} />
                </div>
              ))}
            </div>
          </div>
        );
      }
      
      // Grid virtualizer
      function VirtualGrid({ items, columnCount = 3 }: { items: Item[]; columnCount?: number }) {
        const parentRef = useRef<HTMLDivElement>(null);
        const rowCount = Math.ceil(items.length / columnCount);
      
        const rowVirtualizer = useVirtualizer({
          count: rowCount,
          getScrollElement: () => parentRef.current,
          estimateSize: () => 200,
        });
      
        const columnVirtualizer = useVirtualizer({
          horizontal: true,
          count: columnCount,
          getScrollElement: () => parentRef.current,
          estimateSize: () => 300,
        });
      
        return (
          <div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
            <div
              style={{
                height: rowVirtualizer.getTotalSize(),
                width: columnVirtualizer.getTotalSize(),
                position: 'relative',
              }}
            >
              {rowVirtualizer.getVirtualItems().map(row =>
                columnVirtualizer.getVirtualItems().map(col => {
                  const index = row.index * columnCount + col.index;
                  if (index >= items.length) return null;
                  return (
                    <div
                      key={`${row.key}-${col.key}`}
                      style={{
                        position: 'absolute',
                        top: row.start,
                        left: col.start,
                        width: col.size,
                        height: row.size,
                      }}
                    >
                      <GridItem item={items[index]} />
                    </div>
                  );
                })
              )}
            </div>
          </div>
        );
      }
      ```
      
      ---
      
      ## React Compiler (React 19)
      
      The React Compiler automatically applies memoization — most manual `memo`, `useMemo`, and `useCallback` calls become unnecessary.
      
      ```tsx
      // Before React Compiler — manual memoization
      const ExpensiveList = memo(function ExpensiveList({ items, onDelete }: Props) {
        const sorted = useMemo(() => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items]);
        const handleDelete = useCallback((id: string) => onDelete(id), [onDelete]);
        return sorted.map(item => <Item key={item.id} item={item} onDelete={handleDelete} />);
      });
      
      // After React Compiler — compiler adds memoization automatically
      function ExpensiveList({ items, onDelete }: Props) {
        const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
        return sorted.map(item => <Item key={item.id} item={item} onDelete={onDelete} />);
      }
      
      // Opt out specific components if compiler breaks them
      function ProblematicComponent() {
        "use no memo";
        // ... compiler skips this component
      }
      ```
      
      ### Enabling React Compiler (Next.js)
      
      ```javascript
      // next.config.js
      const nextConfig = {
        experimental: {
          reactCompiler: true,
        },
      };
      
      // babel.config.js (for non-Next.js setups)
      module.exports = {
        plugins: [['babel-plugin-react-compiler', {}]],
      };
      ```
      
      ---
      
      ## Bundle Analysis
      
      ```bash
      # Next.js bundle analyzer
      npm install @next/bundle-analyzer
      
      # next.config.js
      const withBundleAnalyzer = require('@next/bundle-analyzer')({
        enabled: process.env.ANALYZE === 'true',
      });
      module.exports = withBundleAnalyzer({});
      
      # Run
      ANALYZE=true npm run build
      ```
      
      ```bash
      # source-map-explorer (framework-agnostic)
      npm install --save-dev source-map-explorer
      npx source-map-explorer 'build/static/js/*.js'
      ```
      
      ---
      
      ## React DevTools Profiler
      
      ```tsx
      // Mark component interactions for DevTools
      import { Profiler } from 'react';
      
      function onRenderCallback(
        id: string,          // component tree id
        phase: 'mount' | 'update',
        actualDuration: number,  // time spent rendering
        baseDuration: number,    // estimated full render time
        startTime: number,
        commitTime: number
      ) {
        if (actualDuration > 16) { // flag renders > 1 frame (16ms)
          console.warn(`Slow render: ${id} took ${actualDuration.toFixed(2)}ms`);
        }
      }
      
      function App() {
        return (
          <Profiler id="Dashboard" onRender={onRenderCallback}>
            <Dashboard />
          </Profiler>
        );
      }
      ```
      
      ---
      
      ## Web Vitals
      
      | Metric | Meaning | React Impact | Target |
      |--------|---------|-------------|--------|
      | LCP (Largest Contentful Paint) | When main content loads | Large component trees, unoptimized images | < 2.5s |
      | FID / INP (Interaction to Next Paint) | Response time to user input | Long tasks blocking main thread | < 200ms |
      | CLS (Cumulative Layout Shift) | Visual stability | Dynamic content without reserved space | < 0.1 |
      | TTFB (Time to First Byte) | Server response time | RSC data fetching efficiency | < 800ms |
      
      ```tsx
      // Measure Web Vitals in Next.js
      // app/layout.tsx
      export function reportWebVitals(metric: NextWebVitalsMetric) {
        if (metric.label === 'web-vital') {
          // Send to analytics
          analytics.track('web_vital', {
            name: metric.name,
            value: metric.value,
            rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
          });
        }
      }
      
      // Avoiding CLS: always reserve space for dynamic content
      function Avatar({ src }: { src: string }) {
        return (
          // Fixed dimensions prevent layout shift when image loads
          <div style={{ width: 40, height: 40 }}>
            <img src={src} width={40} height={40} alt="" />
          </div>
        );
      }
      ```
      
      ---
      
      ## Image Optimization
      
      ```tsx
      import Image from 'next/image';
      
      // Optimized image with automatic WebP conversion, lazy loading, CLS prevention
      function ProductImage({ product }: { product: Product }) {
        return (
          <div style={{ position: 'relative', aspectRatio: '16/9' }}>
            <Image
              src={product.imageUrl}
              alt={product.name}
              fill                    // fills parent container
              sizes="(max-width: 768px) 100vw, 50vw"  // responsive sizes hint
              priority={false}        // true for above-fold LCP images
              placeholder="blur"      // or "empty"
              blurDataURL={product.blurDataUrl}
            />
          </div>
        );
      }
      
      // LCP image — must be priority
      function HeroImage() {
        return (
          <Image
            src="/hero.jpg"
            alt="Hero"
            width={1200}
            height={600}
            priority          // preload this image — no lazy loading
          />
        );
      }
      ```
      
      ---
      
      ## Performance Anti-patterns
      
      | Anti-pattern | Problem | Fix |
      |--------------|---------|-----|
      | `memo` on everything | Comparison overhead, false optimization | Profile first; only memo when re-renders are measured problem |
      | `useMemo` for cheap computations | Overhead of memoization > cost of computation | Only memoize if computation takes >1ms |
      | `useCallback` without memoized consumers | Stable reference with no benefit | Only use when callback is dep in `useEffect` or passed to `memo` component |
      | No `key` strategy for lists | React unmounts/remounts on reorder | Stable unique IDs from data |
      | Inline object/array props on `memo` components | New reference every render defeats memo | `useMemo` the value or move outside component |
      | Not virtualizing long lists | Renders thousands of DOM nodes | Use `@tanstack/react-virtual` for 100+ items |
      | All JS in single bundle | Slow initial load | Route-based code splitting with `lazy` |
      | `useEffect` polling instead of WebSocket/SSE | Constant network requests | Switch to real-time transport |
      | Importing full lodash/moment | Huge bundle impact | Use tree-shakeable alternatives or native APIs |
      
    • server-components.md 12.4 KB
      # Server Components
      
      React Server Components (RSC), Server Actions, Next.js App Router patterns, caching, and streaming.
      
      ---
      
      ## RSC Architecture
      
      Server Components render on the server and send HTML (and a serialized React tree) to the client. They never ship their code to the browser.
      
      ```
      Request
         │
         ▼
      Server Component Tree (renders on server)
         │
         ├─ Async data fetching (db, fs, fetch)
         ├─ Heavy dependencies (never in client bundle)
         └─ Client Component boundaries (marked 'use client')
                │
                ▼
             Hydration (client takes over interactive parts only)
      ```
      
      **Serialization rules — what can cross the server→client boundary:**
      - Strings, numbers, booleans, null, undefined
      - Arrays and plain objects of the above
      - Promises (unwrapped by `use()` on client)
      - JSX / React elements
      - **NOT**: functions, class instances, Date objects, Maps, Sets, RegExp (must be serialized or passed differently)
      
      ---
      
      ## Server Components
      
      ```tsx
      // app/users/page.tsx — Server Component (default, no directive needed)
      import { db } from '@/lib/db';
      import { cache } from 'react';
      
      // cache() deduplicates calls within a single render pass
      const getUser = cache(async (id: string) => {
        return db.query.users.findFirst({ where: eq(users.id, id) });
      });
      
      // Top-level async component — no useEffect, no loading state needed
      export default async function UsersPage() {
        // Fetch in parallel — both start simultaneously
        const [users, stats] = await Promise.all([
          db.query.users.findMany({ limit: 50 }),
          db.query.stats.findFirst(),
        ]);
      
        return (
          <main>
            <h1>Users ({stats?.total ?? 0})</h1>
            <UserList users={users} />
          </main>
        );
      }
      ```
      
      ### What You Can Do in Server Components
      
      ```tsx
      // 1. Database queries (Drizzle, Prisma, raw SQL)
      const posts = await db.select().from(postsTable).where(eq(postsTable.published, true));
      
      // 2. File system access
      import { readFile } from 'fs/promises';
      const content = await readFile('./data/content.md', 'utf8');
      
      // 3. Server-only secrets (never sent to client)
      const apiData = await fetch('https://api.example.com/data', {
        headers: { Authorization: `Bearer ${process.env.SECRET_API_KEY}` },
      });
      
      // 4. Import heavy libraries without bundle cost
      import { parse } from 'some-huge-parser'; // 2MB — never in client bundle
      const result = parse(rawData);
      
      // 5. Conditional rendering based on server state/permissions
      const session = await auth();
      if (!session?.user) redirect('/login');
      ```
      
      ---
      
      ## Client Components
      
      ```tsx
      // components/counter.tsx
      'use client'; // marks this module and all its imports as client code
      
      import { useState, useEffect } from 'react';
      
      // Anything requiring hooks, browser APIs, or interactivity
      export function Counter({ initialCount = 0 }: { initialCount?: number }) {
        const [count, setCount] = useState(initialCount);
      
        useEffect(() => {
          document.title = `Count: ${count}`;
        }, [count]);
      
        return (
          <div>
            <p>{count}</p>
            <button onClick={() => setCount(c => c + 1)}>Increment</button>
          </div>
        );
      }
      ```
      
      ### Passing Server Data to Client Components
      
      ```tsx
      // Server Component (parent)
      async function ProductPage({ id }: { id: string }) {
        const product = await db.products.findUnique({ where: { id } });
      
        // Pass serializable data as props
        return (
          <div>
            <ProductImages images={product.images} /> {/* Server Component */}
            <AddToCart
              productId={product.id}  // string — serializable
              price={product.price}    // number — serializable
              // onAdd={addToCart}     // ERROR: functions can't cross boundary
            />
          </div>
        );
      }
      
      // Pattern: pass Server Component output as children to Client Component
      async function Layout({ children }: { children: React.ReactNode }) {
        const nav = await buildNavigation(); // server-only fetch
        return (
          <Shell nav={<ServerNav items={nav} />}> {/* Shell is Client Component */}
            {children}
          </Shell>
        );
      }
      ```
      
      ---
      
      ## Server Actions
      
      ```tsx
      // app/actions.ts
      'use server'; // all exports are server actions
      
      import { revalidatePath, revalidateTag } from 'next/cache';
      import { redirect } from 'next/navigation';
      import { z } from 'zod';
      
      const createPostSchema = z.object({
        title: z.string().min(1).max(200),
        content: z.string().min(10),
        published: z.coerce.boolean().default(false),
      });
      
      export async function createPost(formData: FormData) {
        // Validate
        const parsed = createPostSchema.safeParse(Object.fromEntries(formData));
        if (!parsed.success) {
          return { error: parsed.error.flatten().fieldErrors };
        }
      
        // Auth check
        const session = await auth();
        if (!session?.user) throw new Error('Unauthorized');
      
        // Persist
        const post = await db.posts.create({
          data: { ...parsed.data, authorId: session.user.id },
        });
      
        // Invalidate cache
        revalidatePath('/posts');
        revalidateTag('posts');
      
        // Redirect (throws internally, not caught by try/catch)
        redirect(`/posts/${post.id}`);
      }
      
      // Progressive enhancement: works without JS, enhanced with JS
      export async function deletePost(id: string) {
        await db.posts.delete({ where: { id } });
        revalidatePath('/posts');
      }
      ```
      
      ### Form with Server Action
      
      ```tsx
      // app/posts/new/page.tsx
      import { createPost } from '../actions';
      
      // Server Component — no 'use client' needed
      export default function NewPostPage() {
        return (
          <form action={createPost}>
            <input name="title" placeholder="Post title" required />
            <textarea name="content" placeholder="Content" required />
            <label>
              <input type="checkbox" name="published" value="true" />
              Publish immediately
            </label>
            <button type="submit">Create Post</button>
          </form>
        );
      }
      ```
      
      ### Server Action with useActionState (React 19)
      
      ```tsx
      'use client';
      
      import { useActionState } from 'react';
      import { createPost } from '../actions';
      
      type ActionState = { error?: Record<string, string[]>; message?: string } | null;
      
      export function CreatePostForm() {
        const [state, action, isPending] = useActionState<ActionState, FormData>(
          createPost,
          null
        );
      
        return (
          <form action={action}>
            <input name="title" aria-invalid={!!state?.error?.title} />
            {state?.error?.title && <p role="alert">{state.error.title[0]}</p>}
      
            <textarea name="content" />
            {state?.error?.content && <p role="alert">{state.error.content[0]}</p>}
      
            <button disabled={isPending}>
              {isPending ? 'Creating...' : 'Create Post'}
            </button>
          </form>
        );
      }
      ```
      
      ---
      
      ## Next.js App Router File Conventions
      
      ```
      app/
      ├── layout.tsx          # Shared layout (wraps all pages in segment)
      ├── page.tsx            # Route UI (publicly accessible at URL)
      ├── loading.tsx         # Suspense boundary skeleton (automatic)
      ├── error.tsx           # Error boundary fallback (must be 'use client')
      ├── not-found.tsx       # 404 UI (shown by notFound() call)
      ├── route.ts            # API route handler (GET, POST, etc.)
      ├── template.tsx        # Like layout but re-mounts on navigation
      └── (group)/            # Route group — parentheses = no URL segment
          └── dashboard/
              └── page.tsx    # app.com/dashboard
      ```
      
      ```tsx
      // app/layout.tsx
      import { Inter } from 'next/font/google';
      import type { Metadata } from 'next';
      
      const inter = Inter({ subsets: ['latin'] });
      
      export const metadata: Metadata = {
        title: { template: '%s | MyApp', default: 'MyApp' },
        description: 'My application',
      };
      
      export default function RootLayout({ children }: { children: React.ReactNode }) {
        return (
          <html lang="en">
            <body className={inter.className}>{children}</body>
          </html>
        );
      }
      
      // app/posts/[id]/error.tsx — must be Client Component
      'use client';
      
      export default function PostError({
        error,
        reset,
      }: {
        error: Error & { digest?: string };
        reset: () => void;
      }) {
        return (
          <div role="alert">
            <h2>Failed to load post</h2>
            <p>{error.message}</p>
            <button onClick={reset}>Try again</button>
          </div>
        );
      }
      
      // app/api/users/route.ts — API Route Handler
      import { NextRequest, NextResponse } from 'next/server';
      
      export async function GET(request: NextRequest) {
        const { searchParams } = new URL(request.url);
        const limit = Number(searchParams.get('limit') ?? '20');
      
        const users = await db.users.findMany({ take: limit });
        return NextResponse.json(users);
      }
      
      export async function POST(request: NextRequest) {
        const body = await request.json();
        const user = await db.users.create({ data: body });
        return NextResponse.json(user, { status: 201 });
      }
      ```
      
      ---
      
      ## Caching
      
      ```tsx
      // 1. fetch() cache (Next.js extends native fetch)
      async function getPost(id: string) {
        const res = await fetch(`https://api.example.com/posts/${id}`, {
          next: {
            revalidate: 3600, // revalidate every 1 hour (ISR)
            tags: ['posts', `post-${id}`], // tag for on-demand revalidation
          },
          // cache: 'no-store'  // disable caching entirely (always fresh)
          // cache: 'force-cache' // always use cache (default for static)
        });
        return res.json();
      }
      
      // 2. unstable_cache (for non-fetch data sources like ORMs)
      import { unstable_cache } from 'next/cache';
      
      const getCachedUsers = unstable_cache(
        async () => db.users.findMany(),
        ['users-list'],          // cache key
        { revalidate: 300, tags: ['users'] } // 5 min TTL + tag
      );
      
      // 3. On-demand revalidation (Server Action or API route)
      import { revalidatePath, revalidateTag } from 'next/cache';
      
      export async function updatePost(id: string, data: Partial<Post>) {
        await db.posts.update({ where: { id }, data });
      
        revalidateTag(`post-${id}`);    // invalidate specific post cache
        revalidateTag('posts');          // invalidate all posts list cache
        revalidatePath('/posts');        // invalidate path-based cache
        revalidatePath(`/posts/${id}`);
      }
      ```
      
      ---
      
      ## Streaming with Suspense
      
      ```tsx
      // Wrap slow components in Suspense — page loads instantly,
      // slow parts stream in progressively
      import { Suspense } from 'react';
      
      // app/dashboard/page.tsx
      export default function DashboardPage() {
        return (
          <div className="grid">
            {/* Fast — renders immediately */}
            <WelcomeHeader />
      
            {/* Slow DB queries stream in independently */}
            <Suspense fallback={<MetricsSkeleton />}>
              <DashboardMetrics /> {/* async Server Component */}
            </Suspense>
      
            <Suspense fallback={<ActivitySkeleton />}>
              <RecentActivity /> {/* async Server Component */}
            </Suspense>
      
            <Suspense fallback={<ChartSkeleton />}>
              <RevenueChart /> {/* slow, streams last */}
            </Suspense>
          </div>
        );
      }
      
      // loading.tsx provides automatic Suspense for the entire segment
      // app/dashboard/loading.tsx
      export default function DashboardLoading() {
        return <DashboardSkeleton />;
      }
      ```
      
      ---
      
      ## Metadata API
      
      ```tsx
      // Static metadata
      export const metadata: Metadata = {
        title: 'My Page',
        description: 'Page description',
        openGraph: {
          title: 'My Page',
          images: [{ url: '/og-image.png', width: 1200, height: 630 }],
        },
      };
      
      // Dynamic metadata
      export async function generateMetadata(
        { params }: { params: { id: string } }
      ): Promise<Metadata> {
        const post = await getPost(params.id);
        if (!post) return { title: 'Post Not Found' };
      
        return {
          title: post.title,
          description: post.excerpt,
          openGraph: {
            title: post.title,
            images: [{ url: post.coverImage }],
          },
          alternates: {
            canonical: `https://mysite.com/posts/${post.slug}`,
          },
        };
      }
      ```
      
      ---
      
      ## Patterns to Avoid
      
      | Anti-pattern | Problem | Fix |
      |--------------|---------|-----|
      | `'use client'` at root layout | Entire app becomes client-side; no RSC benefits | Push `'use client'` to leaf components only |
      | Waterfall data fetching in Server Components | Each await blocks the next | `Promise.all()` for parallel fetches |
      | No Suspense boundaries | Entire page waits for slowest component | Wrap each async section in `<Suspense>` |
      | Server Action without validation | Security risk, bad UX | Always validate with Zod before DB write |
      | Fetching same data in multiple Server Components | Multiple DB queries for same data | `cache()` wrapper to deduplicate per request |
      | Passing non-serializable data to Client Components | Runtime error | Only pass strings, numbers, plain objects, JSX |
      | Large third-party imports in Client Components | Bloated client bundle | Move to Server Component; import only what's needed |
      | `cookies()` or `headers()` outside Server Components | Runtime error | Only in Server Components, Route Handlers, Server Actions |
      
    • state-management.md 17.7 KB
      # State Management
      
      Comprehensive reference for React state: Context API, Zustand, Jotai, Redux Toolkit, TanStack Query, React Hook Form, and URL state.
      
      ---
      
      ## Decision Matrix
      
      | State Type | Scope | Change Freq | Best Tool |
      |------------|-------|-------------|-----------|
      | Local UI (toggle, form input) | Component | Any | `useState` / `useReducer` |
      | Shared, rarely changes (theme, locale, auth) | App-wide | Low | Context API |
      | Global client state (cart, UI prefs) | App-wide | Medium-High | Zustand |
      | Atomic/fine-grained state | App-wide | High | Jotai |
      | Complex flows, large team, time-travel debug | App-wide | Any | Redux Toolkit |
      | Server data (API responses, cache) | App-wide | External | TanStack Query |
      | Form state | Component | High | React Hook Form |
      | URL-driven state (filters, pagination) | Shareable | Medium | `useSearchParams` / nuqs |
      
      ---
      
      ## Context API
      
      ### Basic Pattern
      
      ```tsx
      import { createContext, useContext, useState, useMemo, ReactNode } from 'react';
      
      interface ThemeContextValue {
        theme: 'light' | 'dark';
        toggleTheme: () => void;
      }
      
      // 1. Create context with null default (enforces provider requirement)
      const ThemeContext = createContext<ThemeContextValue | null>(null);
      
      // 2. Custom hook — single usage point, enforces provider
      export function useTheme(): ThemeContextValue {
        const ctx = useContext(ThemeContext);
        if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
        return ctx;
      }
      
      // 3. Provider — memoize value to prevent unnecessary consumer re-renders
      export function ThemeProvider({ children }: { children: ReactNode }) {
        const [theme, setTheme] = useState<'light' | 'dark'>('light');
      
        // Memoize so object reference only changes when theme changes
        const value = useMemo(
          () => ({ theme, toggleTheme: () => setTheme(t => (t === 'light' ? 'dark' : 'light')) }),
          [theme]
        );
      
        return (
          <ThemeContext.Provider value={value}>
            {children}
          </ThemeContext.Provider>
        );
      }
      ```
      
      ### Performance: Split Contexts by Update Frequency
      
      ```tsx
      // BAD: single context — every consumer re-renders when ANY value changes
      const AppContext = createContext({ user, cart, theme, notifications });
      
      // GOOD: separate contexts — consumers only re-render for what they use
      const UserContext = createContext<User | null>(null);
      const CartContext = createContext<CartState | null>(null);
      const ThemeContext = createContext<Theme>('light');
      
      // BAD: context value recreated every render
      function BadProvider({ children }: { children: ReactNode }) {
        const [count, setCount] = useState(0);
        return (
          // New object reference every render — all consumers re-render!
          <MyContext.Provider value={{ count, setCount }}>
            {children}
          </MyContext.Provider>
        );
      }
      
      // GOOD: memoized value
      function GoodProvider({ children }: { children: ReactNode }) {
        const [count, setCount] = useState(0);
        const value = useMemo(() => ({ count, setCount }), [count]);
        return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
      }
      ```
      
      ---
      
      ## Zustand
      
      Minimal boilerplate, no providers needed, supports middleware.
      
      ### Basic Store
      
      ```typescript
      import { create } from 'zustand';
      import { devtools, persist } from 'zustand/middleware';
      
      interface BearState {
        bears: number;
        increase: (by?: number) => void;
        reset: () => void;
      }
      
      const useBearStore = create<BearState>()(
        devtools(
          persist(
            (set) => ({
              bears: 0,
              increase: (by = 1) => set(state => ({ bears: state.bears + by })),
              reset: () => set({ bears: 0 }),
            }),
            { name: 'bear-storage' } // localStorage key
          ),
          { name: 'BearStore' } // DevTools display name
        )
      );
      
      // Usage — select only what you need to minimize re-renders
      function BearCounter() {
        const bears = useBearStore(state => state.bears);
        return <p>{bears} bears</p>;
      }
      
      function BearControls() {
        const increase = useBearStore(state => state.increase);
        const reset = useBearStore(state => state.reset);
        return (
          <>
            <button onClick={() => increase()}>+1</button>
            <button onClick={() => increase(10)}>+10</button>
            <button onClick={reset}>Reset</button>
          </>
        );
      }
      ```
      
      ### Slices Pattern (Large Stores)
      
      ```typescript
      import { create, StateCreator } from 'zustand';
      
      // Slice 1: auth
      interface AuthSlice {
        user: User | null;
        login: (user: User) => void;
        logout: () => void;
      }
      
      const createAuthSlice: StateCreator<AuthSlice & CartSlice, [], [], AuthSlice> = set => ({
        user: null,
        login: (user) => set({ user }),
        logout: () => set({ user: null }),
      });
      
      // Slice 2: cart
      interface CartSlice {
        items: CartItem[];
        addItem: (item: CartItem) => void;
        removeItem: (id: string) => void;
      }
      
      const createCartSlice: StateCreator<AuthSlice & CartSlice, [], [], CartSlice> = set => ({
        items: [],
        addItem: (item) => set(state => ({ items: [...state.items, item] })),
        removeItem: (id) => set(state => ({ items: state.items.filter(i => i.id !== id) })),
      });
      
      // Combined store
      const useStore = create<AuthSlice & CartSlice>()((...args) => ({
        ...createAuthSlice(...args),
        ...createCartSlice(...args),
      }));
      
      // Focused selectors — each component subscribes to only its slice
      export const useUser = () => useStore(state => state.user);
      export const useCart = () => useStore(state => state.items);
      export const useCartActions = () =>
        useStore(state => ({ addItem: state.addItem, removeItem: state.removeItem }));
      ```
      
      ---
      
      ## Jotai
      
      Atomic state model — compose fine-grained atoms instead of one store.
      
      ```typescript
      import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai';
      import { atomWithStorage, atomWithReset } from 'jotai/utils';
      
      // Primitive atoms
      const countAtom = atom(0);
      const nameAtom = atom('');
      
      // Derived (read-only) atom
      const doubledAtom = atom(get => get(countAtom) * 2);
      
      // Write-only atom
      const incrementAtom = atom(null, (get, set) => {
        set(countAtom, get(countAtom) + 1);
      });
      
      // Async atom — integrates with Suspense
      const userAtom = atom(async () => {
        const res = await fetch('/api/me');
        return res.json() as Promise<User>;
      });
      
      // Persistent atom (localStorage)
      const themeAtom = atomWithStorage<'light' | 'dark'>('theme', 'light');
      
      // Resettable atom
      const filterAtom = atomWithReset({ search: '', category: 'all' });
      
      // Usage
      function Counter() {
        const [count, setCount] = useAtom(countAtom);
        const doubled = useAtomValue(doubledAtom);
        const increment = useSetAtom(incrementAtom);
      
        return (
          <div>
            <p>Count: {count}, Doubled: {doubled}</p>
            <button onClick={increment}>Increment</button>
            <button onClick={() => setCount(0)}>Reset</button>
          </div>
        );
      }
      ```
      
      ---
      
      ## Redux Toolkit
      
      Best for large teams, complex state machines, and when time-travel debugging matters.
      
      ### Slice + Thunk
      
      ```typescript
      import {
        createSlice,
        createAsyncThunk,
        PayloadAction,
        createEntityAdapter,
      } from '@reduxjs/toolkit';
      import type { RootState, AppDispatch } from './store';
      
      // Entity adapter for normalized CRUD
      const usersAdapter = createEntityAdapter<User>();
      
      // Async thunk for data fetching
      export const fetchUsers = createAsyncThunk(
        'users/fetchAll',
        async (_, { rejectWithValue }) => {
          try {
            const res = await fetch('/api/users');
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            return (await res.json()) as User[];
          } catch (err) {
            return rejectWithValue((err as Error).message);
          }
        }
      );
      
      // Slice
      const usersSlice = createSlice({
        name: 'users',
        initialState: usersAdapter.getInitialState({
          status: 'idle' as 'idle' | 'loading' | 'succeeded' | 'failed',
          error: null as string | null,
        }),
        reducers: {
          userAdded: usersAdapter.addOne,
          userUpdated: usersAdapter.updateOne,
          userRemoved: usersAdapter.removeOne,
        },
        extraReducers: builder => {
          builder
            .addCase(fetchUsers.pending, state => {
              state.status = 'loading';
            })
            .addCase(fetchUsers.fulfilled, (state, action) => {
              state.status = 'succeeded';
              usersAdapter.setAll(state, action.payload);
            })
            .addCase(fetchUsers.rejected, (state, action) => {
              state.status = 'failed';
              state.error = action.payload as string;
            });
        },
      });
      
      // Selectors from adapter
      export const { selectAll: selectAllUsers, selectById: selectUserById } =
        usersAdapter.getSelectors((state: RootState) => state.users);
      
      // Custom selectors
      export const selectUsersStatus = (state: RootState) => state.users.status;
      
      export const { userAdded, userUpdated, userRemoved } = usersSlice.actions;
      export default usersSlice.reducer;
      ```
      
      ### RTK Query (preferred over sagas/thunks for data fetching)
      
      ```typescript
      import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
      
      export const apiSlice = createApi({
        reducerPath: 'api',
        baseQuery: fetchBaseQuery({
          baseUrl: '/api',
          prepareHeaders: (headers, { getState }) => {
            const token = (getState() as RootState).auth.token;
            if (token) headers.set('Authorization', `Bearer ${token}`);
            return headers;
          },
        }),
        tagTypes: ['User', 'Post'],
        endpoints: builder => ({
          getUsers: builder.query<User[], void>({
            query: () => '/users',
            providesTags: ['User'],
          }),
          getUserById: builder.query<User, string>({
            query: id => `/users/${id}`,
            providesTags: (result, error, id) => [{ type: 'User', id }],
          }),
          createUser: builder.mutation<User, Partial<User>>({
            query: body => ({ url: '/users', method: 'POST', body }),
            invalidatesTags: ['User'],
          }),
          updateUser: builder.mutation<User, Pick<User, 'id'> & Partial<User>>({
            query: ({ id, ...patch }) => ({ url: `/users/${id}`, method: 'PATCH', body: patch }),
            invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
          }),
        }),
      });
      
      export const {
        useGetUsersQuery,
        useGetUserByIdQuery,
        useCreateUserMutation,
        useUpdateUserMutation,
      } = apiSlice;
      
      // Usage
      function UserList() {
        const { data: users = [], isLoading, isError } = useGetUsersQuery();
        const [createUser, { isLoading: isCreating }] = useCreateUserMutation();
      
        if (isLoading) return <Spinner />;
        if (isError) return <Error />;
      
        return (
          <>
            {users.map(u => <UserCard key={u.id} user={u} />)}
            <button onClick={() => createUser({ name: 'New User' })} disabled={isCreating}>
              Add User
            </button>
          </>
        );
      }
      ```
      
      ---
      
      ## TanStack Query (React Query)
      
      The standard for server state. Handles caching, background refetch, stale-while-revalidate.
      
      ```typescript
      import {
        useQuery,
        useMutation,
        useQueryClient,
        useInfiniteQuery,
        QueryClient,
        QueryClientProvider,
      } from '@tanstack/react-query';
      
      // Setup
      const queryClient = new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000, // data fresh for 1 minute
            retry: 3,
          },
        },
      });
      
      function App() {
        return (
          <QueryClientProvider client={queryClient}>
            <Router />
          </QueryClientProvider>
        );
      }
      
      // Basic query
      function UserProfile({ userId }: { userId: string }) {
        const { data: user, isLoading, error, refetch } = useQuery({
          queryKey: ['users', userId],
          queryFn: () => fetchUser(userId),
          enabled: !!userId, // only run when userId is truthy
          staleTime: 5 * 60 * 1000, // override: 5 minutes
        });
      
        if (isLoading) return <Skeleton />;
        if (error) return <Error onRetry={refetch} />;
        return <div>{user?.name}</div>;
      }
      
      // Mutation with optimistic update
      function DeleteButton({ userId }: { userId: string }) {
        const queryClient = useQueryClient();
      
        const mutation = useMutation({
          mutationFn: (id: string) => deleteUser(id),
      
          // Optimistic update
          onMutate: async (id) => {
            await queryClient.cancelQueries({ queryKey: ['users'] });
            const previousUsers = queryClient.getQueryData<User[]>(['users']);
      
            queryClient.setQueryData<User[]>(['users'], old =>
              old?.filter(u => u.id !== id) ?? []
            );
      
            return { previousUsers }; // context for rollback
          },
      
          // Rollback on error
          onError: (err, id, context) => {
            if (context?.previousUsers) {
              queryClient.setQueryData(['users'], context.previousUsers);
            }
          },
      
          // Always invalidate after settle
          onSettled: () => {
            queryClient.invalidateQueries({ queryKey: ['users'] });
          },
        });
      
        return (
          <button
            onClick={() => mutation.mutate(userId)}
            disabled={mutation.isPending}
          >
            {mutation.isPending ? 'Deleting...' : 'Delete'}
          </button>
        );
      }
      
      // Infinite query (pagination / infinite scroll)
      function PostFeed() {
        const {
          data,
          fetchNextPage,
          hasNextPage,
          isFetchingNextPage,
        } = useInfiniteQuery({
          queryKey: ['posts'],
          queryFn: ({ pageParam }) => fetchPosts({ cursor: pageParam, limit: 20 }),
          initialPageParam: undefined as string | undefined,
          getNextPageParam: lastPage => lastPage.nextCursor,
        });
      
        const posts = data?.pages.flatMap(page => page.posts) ?? [];
      
        return (
          <>
            {posts.map(post => <PostCard key={post.id} post={post} />)}
            <button
              onClick={() => fetchNextPage()}
              disabled={!hasNextPage || isFetchingNextPage}
            >
              {isFetchingNextPage ? 'Loading...' : 'Load More'}
            </button>
          </>
        );
      }
      
      // Prefetch on hover (instant navigation feel)
      function PostLink({ postId }: { postId: string }) {
        const queryClient = useQueryClient();
        return (
          <a
            href={`/posts/${postId}`}
            onMouseEnter={() => queryClient.prefetchQuery({
              queryKey: ['posts', postId],
              queryFn: () => fetchPost(postId),
            })}
          >
            Read More
          </a>
        );
      }
      ```
      
      ---
      
      ## React Hook Form
      
      ```typescript
      import { useForm, Controller, SubmitHandler } from 'react-hook-form';
      import { zodResolver } from '@hookform/resolvers/zod';
      import { z } from 'zod';
      
      // 1. Define schema with Zod
      const profileSchema = z.object({
        name: z.string().min(2, 'Name must be at least 2 characters'),
        email: z.string().email('Invalid email address'),
        age: z.coerce.number().int().min(18, 'Must be at least 18').max(120),
        role: z.enum(['admin', 'user', 'moderator']),
        bio: z.string().max(500).optional(),
      });
      
      type ProfileForm = z.infer<typeof profileSchema>;
      
      // 2. Form component
      function ProfileForm({ onSave }: { onSave: (data: ProfileForm) => Promise<void> }) {
        const {
          register,
          handleSubmit,
          control,
          formState: { errors, isSubmitting, isDirty },
          reset,
        } = useForm<ProfileForm>({
          resolver: zodResolver(profileSchema),
          defaultValues: { name: '', email: '', age: 18, role: 'user' },
        });
      
        const onSubmit: SubmitHandler<ProfileForm> = async data => {
          await onSave(data);
          reset(); // reset to defaultValues after success
        };
      
        return (
          <form onSubmit={handleSubmit(onSubmit)} noValidate>
            <div>
              <label htmlFor="name">Name</label>
              <input
                id="name"
                {...register('name')}
                aria-describedby={errors.name ? 'name-error' : undefined}
                aria-invalid={!!errors.name}
              />
              {errors.name && (
                <span id="name-error" role="alert">{errors.name.message}</span>
              )}
            </div>
      
            {/* Controller for third-party input components */}
            <Controller
              name="role"
              control={control}
              render={({ field }) => (
                <Select
                  value={field.value}
                  onChange={field.onChange}
                  options={['admin', 'user', 'moderator']}
                />
              )}
            />
      
            <button type="submit" disabled={isSubmitting || !isDirty}>
              {isSubmitting ? 'Saving...' : 'Save Profile'}
            </button>
          </form>
        );
      }
      ```
      
      ---
      
      ## URL State
      
      ```typescript
      import { useSearchParams } from 'react-router-dom';
      
      // Built-in useSearchParams (React Router v6)
      function ProductFilters() {
        const [searchParams, setSearchParams] = useSearchParams();
      
        const category = searchParams.get('category') ?? 'all';
        const page = Number(searchParams.get('page') ?? '1');
      
        const setFilter = (key: string, value: string) => {
          setSearchParams(prev => {
            prev.set(key, value);
            if (key !== 'page') prev.set('page', '1'); // reset page on filter change
            return prev;
          });
        };
      
        return (
          <div>
            <select
              value={category}
              onChange={e => setFilter('category', e.target.value)}
            >
              <option value="all">All</option>
              <option value="shoes">Shoes</option>
            </select>
            <Pagination currentPage={page} onPageChange={p => setFilter('page', String(p))} />
          </div>
        );
      }
      
      // nuqs — type-safe URL state (Next.js or any framework)
      // npm install nuqs
      import { useQueryState, parseAsInteger, parseAsString } from 'nuqs';
      
      function FilterPage() {
        const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
        const [search, setSearch] = useQueryState('q', parseAsString.withDefault(''));
      
        return (
          <div>
            <input value={search} onChange={e => setSearch(e.target.value)} />
            <button onClick={() => setPage(p => p + 1)}>Next page ({page})</button>
          </div>
        );
      }
      ```
      
      ---
      
      ## Anti-patterns
      
      | Anti-pattern | Problem | Fix |
      |--------------|---------|-----|
      | `useState` + `useEffect` for server data | Manual loading/error/cache management, stale data | TanStack Query |
      | Single massive Context for all global state | Every consumer re-renders on any change | Split contexts by update frequency |
      | Putting functions in Context value without `useMemo` | New object reference every render | `useMemo` the value |
      | Zustand selectors that return objects | New object reference every call triggers re-render | Select primitives; or use `shallow` equality: `useStore(state => state.items, shallow)` |
      | `useEffect` to sync two pieces of state | Double render, complexity | Derive state during render or use `useReducer` |
      | Redux for everything including server data | Over-normalized, async complexity | RTK Query or TanStack Query for server state |
      | No staleTime in TanStack Query | Constant background refetches on every mount | Set appropriate `staleTime` per query |
      
    • testing.md 14.6 KB
      # Testing
      
      React Testing Library, user-event, MSW, Vitest setup, hook testing, and accessibility testing.
      
      ---
      
      ## Philosophy
      
      Test behavior, not implementation. Tests should resemble how users interact with your app.
      
      - Query by what the user sees (role, label, text) — not by class names or IDs
      - Interact the way users do (click, type, submit) — not by calling component methods
      - Assert what the user sees as the outcome — not component state
      
      ---
      
      ## Vitest + RTL Setup
      
      ```bash
      npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
      ```
      
      ```typescript
      // vitest.config.ts
      import { defineConfig } from 'vitest/config';
      import react from '@vitejs/plugin-react';
      
      export default defineConfig({
        plugins: [react()],
        test: {
          environment: 'jsdom',
          globals: true,           // no import { describe, it, expect } needed
          setupFiles: ['./src/test/setup.ts'],
          coverage: {
            provider: 'v8',
            reporter: ['text', 'lcov'],
            exclude: ['**/*.stories.tsx', '**/index.ts'],
          },
        },
      });
      ```
      
      ```typescript
      // src/test/setup.ts
      import '@testing-library/jest-dom'; // extends expect with DOM matchers
      import { cleanup } from '@testing-library/react';
      import { afterEach, beforeAll, afterAll } from 'vitest';
      import { server } from './mocks/server';
      
      // RTL cleanup after each test
      afterEach(() => cleanup());
      
      // MSW lifecycle
      beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
      afterEach(() => server.resetHandlers());
      afterAll(() => server.close());
      ```
      
      ---
      
      ## Rendering and Queries
      
      ### Query Priority
      
      ```
      1. getByRole         — semantic HTML, accessible name
      2. getByLabelText    — form labels
      3. getByPlaceholderText — input placeholders (prefer label)
      4. getByText         — visible text content
      5. getByDisplayValue — current input value
      6. getByAltText      — img alt text
      7. getByTitle        — title attribute
      8. getByTestId       — last resort: data-testid="..."
      ```
      
      ### getBy vs queryBy vs findBy
      
      | Variant | Returns | Throws | Async |
      |---------|---------|--------|-------|
      | `getBy*` | Element | If not found | No |
      | `queryBy*` | Element or null | No | No |
      | `findBy*` | Promise<Element> | If timeout | Yes |
      | `getAllBy*` | Element[] | If none found | No |
      | `queryAllBy*` | Element[] | No | No |
      | `findAllBy*` | Promise<Element[]> | If timeout | Yes |
      
      ```tsx
      import { render, screen } from '@testing-library/react';
      
      test('renders user profile', () => {
        render(<UserProfile user={{ name: 'Alice', role: 'admin' }} />);
      
        // Role-based (preferred) — uses ARIA roles
        expect(screen.getByRole('heading', { name: 'Alice' })).toBeInTheDocument();
        expect(screen.getByRole('button', { name: /edit profile/i })).toBeEnabled();
      
        // Text content
        expect(screen.getByText('admin')).toBeInTheDocument();
      
        // For content that should NOT be present
        expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
      });
      ```
      
      ---
      
      ## User Interactions
      
      Always use `@testing-library/user-event` over `fireEvent` — it simulates real browser events including pointer events, focus, keyboard navigation.
      
      ```tsx
      import { render, screen } from '@testing-library/react';
      import userEvent from '@testing-library/user-event';
      
      describe('LoginForm', () => {
        // Create user instance once per test — manages pointer state
        const user = userEvent.setup();
      
        test('submits with valid credentials', async () => {
          const onLogin = vi.fn();
          render(<LoginForm onLogin={onLogin} />);
      
          // Type into inputs (fires focus, input, change, keydown/up events)
          await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
          await user.type(screen.getByLabelText(/password/i), 'password123');
      
          // Click submit
          await user.click(screen.getByRole('button', { name: /sign in/i }));
      
          expect(onLogin).toHaveBeenCalledWith({
            email: 'alice@example.com',
            password: 'password123',
          });
        });
      
        test('shows validation error for empty email', async () => {
          render(<LoginForm onLogin={vi.fn()} />);
      
          // Tab to trigger blur validation without typing
          await user.click(screen.getByLabelText(/email/i));
          await user.tab();
      
          expect(screen.getByRole('alert')).toHaveTextContent(/email is required/i);
        });
      
        test('disables submit while loading', async () => {
          render(<LoginForm onLogin={() => new Promise(() => {})} />); // never resolves
      
          await user.type(screen.getByLabelText(/email/i), 'alice@example.com');
          await user.type(screen.getByLabelText(/password/i), 'pass');
          await user.click(screen.getByRole('button', { name: /sign in/i }));
      
          expect(screen.getByRole('button', { name: /signing in/i })).toBeDisabled();
        });
      });
      ```
      
      ### Select, Keyboard, Upload
      
      ```tsx
      // Select dropdown
      await user.selectOptions(screen.getByRole('combobox', { name: /country/i }), 'Canada');
      expect(screen.getByRole('option', { name: 'Canada' })).toBeSelected();
      
      // Keyboard shortcuts
      await user.keyboard('{Escape}');          // press Escape
      await user.keyboard('{Control>}k{/Control}'); // Ctrl+K
      
      // File upload
      const file = new File(['content'], 'test.pdf', { type: 'application/pdf' });
      await user.upload(screen.getByLabelText(/upload/i), file);
      
      // Clear an input
      await user.clear(screen.getByRole('textbox', { name: /search/i }));
      ```
      
      ---
      
      ## Async Testing
      
      ```tsx
      import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
      
      test('loads and displays users', async () => {
        render(<UserList />);
      
        // Assert loading state
        expect(screen.getByRole('status')).toHaveTextContent(/loading/i);
      
        // Wait for async operation to complete
        await waitFor(() => {
          expect(screen.queryByRole('status')).not.toBeInTheDocument();
        });
      
        // Alternatively: wait for element to disappear
        await waitForElementToBeRemoved(() => screen.queryByRole('status'));
      
        // Assert loaded state
        expect(screen.getByRole('list')).toBeInTheDocument();
        expect(screen.getAllByRole('listitem')).toHaveLength(3);
      });
      
      // findBy* — combines waitFor + getBy
      test('shows error on failed load', async () => {
        server.use(
          http.get('/api/users', () => HttpResponse.error())
        );
      
        render(<UserList />);
      
        // findBy waits up to 1000ms by default
        const error = await screen.findByRole('alert');
        expect(error).toHaveTextContent(/failed to load/i);
      });
      ```
      
      ---
      
      ## Custom Render with Providers
      
      ```tsx
      // src/test/utils.tsx
      import { render, RenderOptions } from '@testing-library/react';
      import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
      import { MemoryRouter } from 'react-router-dom';
      import { ReactNode } from 'react';
      
      interface WrapperOptions {
        initialRoute?: string;
      }
      
      function createWrapper({ initialRoute = '/' }: WrapperOptions = {}) {
        const queryClient = new QueryClient({
          defaultOptions: {
            queries: { retry: false },    // no retries in tests
            mutations: { retry: false },
          },
        });
      
        return function Wrapper({ children }: { children: ReactNode }) {
          return (
            <QueryClientProvider client={queryClient}>
              <MemoryRouter initialEntries={[initialRoute]}>
                {children}
              </MemoryRouter>
            </QueryClientProvider>
          );
        };
      }
      
      // Custom render — drop-in replacement for RTL's render
      function customRender(
        ui: React.ReactElement,
        options: WrapperOptions & Omit<RenderOptions, 'wrapper'> = {}
      ) {
        const { initialRoute, ...renderOptions } = options;
        return render(ui, {
          wrapper: createWrapper({ initialRoute }),
          ...renderOptions,
        });
      }
      
      // Re-export everything from RTL so tests only need to import from here
      export * from '@testing-library/react';
      export { customRender as render };
      ```
      
      ```tsx
      // Usage in tests — exact same API as RTL
      import { render, screen } from '../test/utils';
      
      test('navigates to profile', async () => {
        const user = userEvent.setup();
        render(<App />, { initialRoute: '/dashboard' });
      
        await user.click(screen.getByRole('link', { name: /profile/i }));
        expect(screen.getByRole('heading', { name: /your profile/i })).toBeInTheDocument();
      });
      ```
      
      ---
      
      ## MSW (Mock Service Worker)
      
      MSW intercepts real network requests — no mocking of fetch/axios needed.
      
      ```typescript
      // src/test/mocks/handlers.ts
      import { http, HttpResponse } from 'msw';
      
      const mockUsers: User[] = [
        { id: '1', name: 'Alice', email: 'alice@example.com' },
        { id: '2', name: 'Bob', email: 'bob@example.com' },
      ];
      
      export const handlers = [
        // GET /api/users
        http.get('/api/users', () => {
          return HttpResponse.json(mockUsers);
        }),
      
        // GET /api/users/:id
        http.get('/api/users/:id', ({ params }) => {
          const user = mockUsers.find(u => u.id === params.id);
          if (!user) return new HttpResponse(null, { status: 404 });
          return HttpResponse.json(user);
        }),
      
        // POST /api/users
        http.post('/api/users', async ({ request }) => {
          const body = await request.json() as Partial<User>;
          const newUser = { id: crypto.randomUUID(), ...body } as User;
          return HttpResponse.json(newUser, { status: 201 });
        }),
      
        // DELETE /api/users/:id
        http.delete('/api/users/:id', ({ params }) => {
          return new HttpResponse(null, { status: 204 });
        }),
      ];
      ```
      
      ```typescript
      // src/test/mocks/server.ts
      import { setupServer } from 'msw/node';
      import { handlers } from './handlers';
      
      export const server = setupServer(...handlers);
      ```
      
      ```tsx
      // Override handlers in specific tests
      import { server } from '../test/mocks/server';
      import { http, HttpResponse } from 'msw';
      
      test('shows error when API fails', async () => {
        // Override default handler for this test only
        server.use(
          http.get('/api/users', () => {
            return HttpResponse.json({ message: 'Internal Server Error' }, { status: 500 });
          })
        );
      
        render(<UserList />);
        await screen.findByRole('alert');
        expect(screen.getByRole('alert')).toHaveTextContent(/something went wrong/i);
      });
      ```
      
      ---
      
      ## Hook Testing
      
      ```tsx
      import { renderHook, act } from '@testing-library/react';
      import { useCounter } from './useCounter';
      
      test('useCounter increments correctly', () => {
        const { result } = renderHook(() => useCounter(0));
      
        expect(result.current.count).toBe(0);
      
        act(() => result.current.increment());
        expect(result.current.count).toBe(1);
      
        act(() => result.current.incrementBy(5));
        expect(result.current.count).toBe(6);
      
        act(() => result.current.reset());
        expect(result.current.count).toBe(0);
      });
      
      // Test hooks that use context
      test('useTheme reads from ThemeProvider', () => {
        const wrapper = ({ children }: { children: React.ReactNode }) => (
          <ThemeProvider initialTheme="dark">{children}</ThemeProvider>
        );
      
        const { result } = renderHook(() => useTheme(), { wrapper });
        expect(result.current.theme).toBe('dark');
      
        act(() => result.current.toggleTheme());
        expect(result.current.theme).toBe('light');
      });
      
      // Test async hooks
      test('useFetch returns data', async () => {
        const { result } = renderHook(() => useFetch<User[]>('/api/users'));
      
        expect(result.current.isLoading).toBe(true);
      
        await waitFor(() => {
          expect(result.current.isLoading).toBe(false);
        });
      
        expect(result.current.data).toHaveLength(2);
        expect(result.current.error).toBeNull();
      });
      ```
      
      ---
      
      ## Component Testing Patterns
      
      ### Modal
      
      ```tsx
      test('modal opens and closes', async () => {
        const user = userEvent.setup();
        render(<DeleteConfirmation onDelete={vi.fn()} />);
      
        // Modal should not be in DOM initially
        expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
      
        await user.click(screen.getByRole('button', { name: /delete/i }));
        expect(screen.getByRole('dialog')).toBeInTheDocument();
        expect(screen.getByRole('dialog')).toHaveAccessibleName(/confirm deletion/i);
      
        await user.click(screen.getByRole('button', { name: /cancel/i }));
        await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
      });
      ```
      
      ### Form Validation
      
      ```tsx
      test('validates required fields on submit', async () => {
        const user = userEvent.setup();
        const onSubmit = vi.fn();
        render(<ContactForm onSubmit={onSubmit} />);
      
        // Submit empty form
        await user.click(screen.getByRole('button', { name: /submit/i }));
      
        // Errors appear
        expect(screen.getByText(/name is required/i)).toBeInTheDocument();
        expect(screen.getByText(/email is required/i)).toBeInTheDocument();
      
        // Form was not submitted
        expect(onSubmit).not.toHaveBeenCalled();
      });
      ```
      
      ---
      
      ## Accessibility Testing
      
      ```tsx
      import { axe, toHaveNoViolations } from 'jest-axe';
      
      expect.extend(toHaveNoViolations());
      
      test('has no accessibility violations', async () => {
        const { container } = render(<LoginForm onLogin={vi.fn()} />);
        const results = await axe(container);
        expect(results).toHaveNoViolations();
      });
      
      // Test keyboard navigation
      test('modal is keyboard accessible', async () => {
        const user = userEvent.setup();
        render(<Modal isOpen onClose={vi.fn()} title="Confirm">Content</Modal>);
      
        const dialog = screen.getByRole('dialog');
      
        // Dialog should have correct ARIA attributes
        expect(dialog).toHaveAttribute('aria-modal', 'true');
        expect(dialog).toHaveAccessibleName('Confirm');
      
        // Escape closes modal
        await user.keyboard('{Escape}');
        // ... assert closed
      });
      
      // Test screen reader text
      test('icon button has accessible name', () => {
        render(<button aria-label="Close menu"><XIcon /></button>);
        expect(screen.getByRole('button', { name: /close menu/i })).toBeInTheDocument();
      });
      ```
      
      ---
      
      ## Snapshot Testing
      
      Use sparingly — for stable UI components where visual regression is more important than behavior.
      
      ```tsx
      // PREFER behavioral tests over snapshots
      // Use snapshots only for:
      // - Stable design system components (Button, Badge, Avatar)
      // - Complex SVG/icon output
      // - Error messages with specific formatting
      
      import { render } from '@testing-library/react';
      
      test('Badge renders correctly', () => {
        const { container } = render(<Badge variant="success" count={5} />);
        expect(container.firstChild).toMatchSnapshot();
      });
      
      // Update snapshots when intentional changes are made:
      // vitest --update-snapshots
      ```
      
      ---
      
      ## Anti-patterns
      
      | Anti-pattern | Problem | Fix |
      |--------------|---------|-----|
      | Query by CSS class or id | Brittle, implementation detail | Query by role, label, or text |
      | `fireEvent` instead of `userEvent` | Doesn't fire real browser events | Use `@testing-library/user-event` |
      | Testing internal state | Tests break on refactor | Test rendered output and behavior |
      | Mocking React components | Hides integration bugs | Test with real components; mock network instead |
      | No async awaiting | Tests pass before assertions run | Always `await` user interactions and async queries |
      | `data-testid` as first choice | Couples tests to implementation | Last resort after semantic queries fail |
      | Test per implementation detail | Brittle test suite | Test per user story / behavior |
      | No error case tests | Only happy path covered | Test loading, error, empty, and edge states |
      
  • scripts
    • .gitkeep 0 B · in bundle
    • check-react-facts.py 11 KB
      #!/usr/bin/env python3
      """Staleness verifier for react-ops: the React 19 facts the skill encodes must
      stay real and named in the prose.
      
      react-ops centers on React 19 (use(), Actions, useActionState, useFormStatus,
      useOptimistic, React Compiler) and names an ecosystem stack (Zustand, Jotai,
      Redux Toolkit, TanStack Query, React Hook Form, Zod). That is exactly the fact
      that drifts silently (SKILL-RESOURCE-PROTOCOL.md §7): a package moves a major
      version upstream, or the prose stops mentioning a package the catalog lists, and
      nobody notices for months. Two modes guard it:
      
        --offline (default, safe for PR CI): structural consistency, no network.
          * assets/react-facts.json parses and every package + version gate is named
            somewhere in the skill prose (SKILL.md / references/*.md) — the catalog
            can't drift from the docs
          * SKILL.md still carries a dated "as of <year>" currency note
        --live (scheduled freshness.yml, never a PR gate): query the npm registry for
          each package's latest dist-tag; flag DRIFT when the live major is newer than
          the documented major (the skill is now behind), or when a package is gone
          (404). Transient registry failure is UNAVAILABLE (exit 7), never a failure.
      
      Usage:   check-react-facts.py [--offline | --live] [--facts FILE] [--skill DIR] [--json] [--timeout S]
      Input:   argv flags only (no stdin).
      Output:  stdout = findings (plain rows, or a --json envelope). Data only.
      Stderr:  the verdict line, notices, errors.
      Exit:    0 ok, 2 usage, 3 facts/skill missing, 4 facts unparseable,
               7 npm registry unreachable (live, advisory — never a real failure),
               10 drift (offline: uncited/undocumented/missing note; live: major ahead or gone)
      
      Examples:
        check-react-facts.py --offline                 # PR CI: catalog ⇆ prose consistency
        check-react-facts.py --live                     # weekly: is any documented major behind npm?
        check-react-facts.py --offline --json | jq '.data[]'
      """
      from __future__ import annotations
      
      import argparse
      import json
      import os
      import re
      import sys
      import urllib.error
      import urllib.parse
      import urllib.request
      from pathlib import Path
      
      EX_OK = 0
      EX_USAGE = 2
      EX_NOTFOUND = 3
      EX_UNPARSEABLE = 4
      EX_UNAVAILABLE = 7
      EX_DRIFT = 10
      
      SCHEMA = "claude-mods.react-ops.facts/v1"
      HERE = Path(__file__).resolve().parent
      DEFAULT_FACTS = HERE.parent / "assets" / "react-facts.json"
      DEFAULT_SKILL = HERE.parent
      REGISTRY = "https://registry.npmjs.org"
      CURRENCY_RE = re.compile(r"as of 20\d\d")
      
      
      class Term:
          """Minimal ANSI helper. Honors FORCE_COLOR / NO_COLOR / TERM_ASCII and the
          bound stream's TTY + encoding so piped data stays plain ASCII."""
      
          _C = {"green": "\033[32m", "red": "\033[31m", "dim": "\033[2m", "off": "\033[0m"}
      
          def __init__(self, stream=sys.stderr):
              enc = (getattr(stream, "encoding", "") or "").lower()
              self.ascii = os.environ.get("TERM_ASCII") == "1" or "utf" not in enc
              if os.environ.get("FORCE_COLOR"):
                  self.color = True
              elif (os.environ.get("NO_COLOR") is not None
                    or os.environ.get("TERM") == "dumb"
                    or not getattr(stream, "isatty", lambda: False)()):
                  self.color = False
              else:
                  self.color = True
      
          def c(self, name, text):
              return f"{self._C.get(name, '')}{text}{self._C['off']}" if self.color else text
      
          def mark(self, ok):
              g = ("+" if self.ascii else "✓") if ok else ("x" if self.ascii else "✗")
              return self.c("green" if ok else "red", g)
      
      
      def load_facts(path: Path) -> dict:
          if not path.is_file():
              print(f"error: facts catalog not found: {path}", file=sys.stderr)
              raise SystemExit(EX_NOTFOUND)
          try:
              data = json.loads(path.read_text(encoding="utf-8"))
              if data.get("schema") != SCHEMA:
                  raise ValueError(f"schema {data.get('schema')!r} != {SCHEMA!r}")
              if not isinstance(data.get("packages"), dict) or not data["packages"]:
                  raise ValueError("'packages' must be a non-empty object")
              for name, info in data["packages"].items():
                  if not isinstance(info, dict) or "documented_major" not in info:
                      raise ValueError(f"package {name!r} missing documented_major")
                  if not isinstance(info.get("prose"), list) or not info["prose"]:
                      raise ValueError(f"package {name!r} missing prose tokens")
              return data
          except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
              print(f"error: could not parse facts {path}: {exc}", file=sys.stderr)
              raise SystemExit(EX_UNPARSEABLE)
      
      
      def read_corpus(skill_dir: Path) -> tuple[str, str]:
          """Returns (skill_md_text, all_prose_text) across SKILL.md + references/*.md."""
          doc = skill_dir / "SKILL.md"
          if not doc.is_file():
              print(f"error: SKILL.md not found under {skill_dir}", file=sys.stderr)
              raise SystemExit(EX_NOTFOUND)
          skill_md = doc.read_text(encoding="utf-8", errors="replace")
          parts = [skill_md]
          for ref in sorted((skill_dir / "references").glob("*.md")):
              parts.append(ref.read_text(encoding="utf-8", errors="replace"))
          return skill_md, "\n".join(parts)
      
      
      def check_offline(facts: dict, skill_dir: Path) -> list[dict]:
          skill_md, corpus = read_corpus(skill_dir)
          findings: list[dict] = []
          for name, info in facts["packages"].items():
              for token in info["prose"]:
                  if token not in corpus:
                      findings.append({"package": name, "issue": f"prose token {token!r} not named in skill"})
          for key, token in facts.get("version_gates", {}).items():
              if key == "_comment":
                  continue
              if str(token) not in corpus:
                  findings.append({"package": "(gate)", "issue": f"version gate {key}={token!r} not stated in skill prose"})
          if not CURRENCY_RE.search(skill_md):
              findings.append({"package": "(SKILL.md)", "issue": "no dated 'as of <year>' currency note"})
          return findings
      
      
      def npm_latest(name: str, timeout: float) -> tuple[str, object]:
          """Return (resolved|notfound|unavailable, version-string-or-status)."""
          url = f"{REGISTRY}/{urllib.parse.quote(name, safe='')}/latest"
          req = urllib.request.Request(url, method="GET",
                                       headers={"User-Agent": "claude-mods-react-ops-check/1",
                                                "Accept": "application/json"})
          try:
              with urllib.request.urlopen(req, timeout=timeout) as resp:
                  manifest = json.loads(resp.read().decode("utf-8"))
                  return ("resolved", manifest.get("version", ""))
          except urllib.error.HTTPError as exc:
              if exc.code in (404, 410):
                  return ("notfound", exc.code)
              return ("unavailable", exc.code)
          except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc:
              return ("unavailable", str(getattr(exc, "reason", exc)))
      
      
      def major_of(version: str) -> int | None:
          m = re.match(r"\d+", version.strip())
          return int(m.group(0)) if m else None
      
      
      def check_live(facts: dict, timeout: float) -> tuple[list[dict], list[dict]]:
          drift: list[dict] = []
          unreachable: list[dict] = []
          for name, info in facts["packages"].items():
              documented = info["documented_major"]
              status, info2 = npm_latest(name, timeout)
              if status == "notfound":
                  drift.append({"package": name, "issue": "no longer resolves on npm (404) — renamed/removed"})
              elif status == "unavailable":
                  unreachable.append({"package": name, "issue": f"registry unreachable: {info2}"})
              else:
                  live = major_of(str(info2))
                  if live is None:
                      unreachable.append({"package": name, "issue": f"could not parse version {info2!r}"})
                  elif live > documented:
                      drift.append({"package": name,
                                    "issue": f"live major {live} ({info2}) ahead of documented major {documented}"})
          return drift, unreachable
      
      
      def main(argv: list[str]) -> int:
          p = argparse.ArgumentParser(
              prog="check-react-facts.py",
              description="Verify react-ops' React 19 facts stay named (offline) and current on npm (live).",
          )
          mode = p.add_mutually_exclusive_group()
          mode.add_argument("--offline", action="store_true", help="structural consistency, no network (default)")
          mode.add_argument("--live", action="store_true", help="check each package's npm major vs documented")
          p.add_argument("--facts", default=str(DEFAULT_FACTS), help="facts catalog JSON")
          p.add_argument("--skill", default=str(DEFAULT_SKILL), help="skill directory (SKILL.md + references/)")
          p.add_argument("--timeout", type=float, default=10.0, help="per-request timeout seconds (live)")
          p.add_argument("--json", action="store_true", help="emit a JSON envelope")
          try:
              args = p.parse_args(argv)
          except SystemExit as exc:
              return EX_USAGE if exc.code not in (0, None) else (exc.code or EX_OK)
      
          facts = load_facts(Path(args.facts))
          live = args.live and not args.offline
          t = Term(sys.stderr)
      
          if live:
              drift, unreachable = check_live(facts, args.timeout)
              findings = drift + unreachable
              if args.json:
                  print(json.dumps({
                      "data": findings,
                      "meta": {"mode": "live", "packages_checked": len(facts["packages"]),
                               "drift": len(drift), "unreachable": len(unreachable),
                               "registry": REGISTRY, "schema": SCHEMA},
                  }, indent=2))
              else:
                  for f in findings:
                      kind = "DRIFT" if f in drift else "UNREACH"
                      print(f"{kind}  {f['package']}: {f['issue']}")
              if drift:
                  print(f"{t.mark(False)} react-facts/live: {len(drift)} package(s) drifted "
                        f"{t.c('dim', '(' + REGISTRY + ')')}", file=sys.stderr)
                  return EX_DRIFT
              if unreachable:
                  print(f"{t.mark(False)} react-facts/live: npm unreachable for "
                        f"{len(unreachable)}/{len(facts['packages'])} {t.c('dim', '(advisory - retry next run)')}",
                        file=sys.stderr)
                  return EX_UNAVAILABLE
              print(f"{t.mark(True)} react-facts/live: all {len(facts['packages'])} package(s) "
                    f"at or below documented major", file=sys.stderr)
              return EX_OK
      
          # offline (default)
          findings = check_offline(facts, Path(args.skill))
          if args.json:
              print(json.dumps({
                  "data": findings,
                  "meta": {"mode": "offline", "packages_checked": len(facts["packages"]),
                           "drift": len(findings), "consistent": not findings, "schema": SCHEMA},
              }, indent=2))
          else:
              for f in findings:
                  print(f"DRIFT  {f['package']}: {f['issue']}")
          ok = not findings
          print(f"{t.mark(ok)} react-facts/offline: {len(facts['packages'])} package(s) + "
                f"{sum(1 for k in facts.get('version_gates', {}) if k != '_comment')} gate(s) checked, "
                f"{len(findings)} inconsistency {t.c('dim', '(catalog vs skill prose)')}", file=sys.stderr)
          return EX_DRIFT if findings else EX_OK
      
      
      if __name__ == "__main__":
          sys.exit(main(sys.argv[1:]))
      
  • tests
    • run.sh 3.5 KB
      #!/usr/bin/env bash
      # Offline self-test for the react-ops skill — structure, frontmatter, and the
      # staleness-verifier contract (SKILL-RESOURCE-PROTOCOL.md §7, §10).
      #
      # Offline-deterministic (no network, no React install). Resolves paths relative
      # to itself so it works in the repo and once installed to ~/.claude/skills/.
      #
      # Usage:   bash tests/run.sh
      # Input:   none (self-contained; no network)
      # Output:  TAP-ish progress on stderr; final PASS/FAIL line.
      # Exit:    0 all pass, 1 any failure
      set -uo pipefail
      
      HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
      SKILL="$(dirname "$HERE")"
      DOC="$SKILL/SKILL.md"
      
      PASS=0; FAIL=0
      ok() { PASS=$((PASS+1)); printf '  PASS  %s\n' "$1" >&2; }
      no() { FAIL=$((FAIL+1)); printf '  FAIL  %s\n' "$1" >&2; }
      
      # Resolve a *working* python (python3, else python). The bare `command -v` is
      # not enough on Windows, where `python3` is a Microsoft Store stub that exits
      # nonzero. Skip the whole verifier block if none works.
      PY=""
      for c in python3 python py; do
        if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PY="$c"; break; fi
      done
      
      echo "=== react-ops self-test ===" >&2
      
      # ── SKILL.md frontmatter ───────────────────────────────────────────────────
      [[ -f "$DOC" ]] && ok "SKILL.md present" || { no "SKILL.md missing"; echo "=== $PASS passed, $FAIL failed ===" >&2; exit 1; }
      doc="$(cat "$DOC")"
      case "$doc" in *"name: react-ops"*) ok "frontmatter declares name: react-ops";; *) no "frontmatter name != react-ops";; esac
      case "$doc" in *"license: MIT"*) ok "frontmatter declares license: MIT";; *) no "missing license: MIT";; esac
      case "$doc" in *"as of 20"*) ok "currency note carries a year";; *) no "no dated 'as of <year>' currency note";; esac
      
      # ── resources present + cited ──────────────────────────────────────────────
      for res in assets/react-facts.json scripts/check-react-facts.py; do
        [[ -f "$SKILL/$res" ]] && ok "resource present: $res" || no "missing resource: $res"
      done
      case "$doc" in *"scripts/check-react-facts.py"*) ok "verifier cited from SKILL.md";; *) no "verifier uncited";; esac
      
      # ── staleness verifier: offline contract (§7) ───────────────────────────────
      if [[ -n "$PY" ]]; then
        V="$SKILL/scripts/check-react-facts.py"
        F="$SKILL/assets/react-facts.json"
        ec() { local want="$1" lbl="$2"; shift 2; "$@" >/dev/null 2>&1; local got=$?
               [[ "$got" == "$want" ]] && ok "$lbl (exit $got)" || no "$lbl (want $want got $got)"; }
        TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
        ec 0 "py_compile"            "$PY" -m py_compile "$V"
        ec 0 "--help"                "$PY" "$V" --help
        ec 0 "--offline consistent"  "$PY" "$V" --offline
        ec 2 "bad flag -> 2"         "$PY" "$V" --bogus
        ec 2 "conflicting modes -> 2" "$PY" "$V" --offline --live
        jout="$("$PY" "$V" --offline --json 2>/dev/null)"
        case "$jout" in *"claude-mods.react-ops.facts/v1"*) ok "--json envelope schema";; *) no "--json envelope schema missing";; esac
        ec 3 "missing facts -> 3"    "$PY" "$V" --offline --facts "$TMP/nope.json"
        printf '{"schema":"claude-mods.react-ops.facts/v1","packages":{"zzz":{"documented_major":1,"prose":["zzznotreal"]}}}' > "$TMP/drift.json"
        ec 10 "uncited package -> 10" "$PY" "$V" --offline --facts "$TMP/drift.json"
      else
        no "no working python to exercise the verifier"
      fi
      
      echo "=== $PASS passed, $FAIL failed ===" >&2
      [[ "$FAIL" -eq 0 ]] || exit 1
      
  • SKILL.md 12.9 KB
    ---
    name: react-ops
    description: "React development patterns, hooks, state management, Server Components, and performance optimization. Use for: react, hooks, useState, useEffect, jsx, tsx, server components, RSC, zustand, react query, component patterns, react testing library, error boundary, suspense, react 19."
    license: MIT
    allowed-tools: "Read Write Bash"
    metadata:
      author: claude-mods
      related-skills: nextjs-ops, typescript-ops, testing-ops, tailwind-ops, javascript-ops
    ---
    
    # React Operations
    
    Comprehensive React skill covering hooks, component architecture, state management, Server Components, and performance optimization.
    
    > React 19 ecosystem facts verified as of 2026-07.
    
    ## Hook Selection Decision Tree
    
    ```
    What problem are you solving?
    │
    ├─ Storing UI state that triggers re-renders
    │  ├─ Simple value (string, number, boolean)
    │  │  └─ useState
    │  ├─ Complex state with multiple sub-values and logic
    │  │  └─ useReducer (actions + reducer = predictable transitions)
    │  └─ Derived from existing state
    │     └─ Calculate inline or useMemo — not useState
    │
    ├─ Referencing a value WITHOUT triggering re-render
    │  ├─ DOM element reference
    │  │  └─ useRef<HTMLElement>(null) + ref={ref}
    │  └─ Mutable value (timer ID, previous value, counter)
    │     └─ useRef (mutate ref.current directly)
    │
    ├─ Running a side effect
    │  ├─ After every render (or specific deps)
    │  │  ├─ Needs cleanup (subscription, timer, abort)
    │  │  │  └─ useEffect with return cleanup function
    │  │  └─ No cleanup (logging, analytics)
    │  │     └─ useEffect with empty or dep array
    │  ├─ Before browser paint (DOM mutation, animation)
    │  │  └─ useLayoutEffect
    │  └─ Triggered by user action (not render)
    │     └─ Call it directly in the event handler — not useEffect
    │
    ├─ Caching an expensive computation
    │  └─ useMemo(() => expensiveCalc(a, b), [a, b])
    │
    ├─ Stable callback reference for child props / event handlers
    │  └─ useCallback(() => doThing(dep), [dep])
    │
    ├─ Reading shared context value
    │  └─ useContext(MyContext)
    │
    ├─ Generating stable unique ID (forms, aria)
    │  └─ useId()
    │
    ├─ Syncing external store (Redux, Zustand internals)
    │  └─ useSyncExternalStore(subscribe, getSnapshot)
    │
    └─ React 19+
       ├─ Await a promise or read context
       │  └─ use(promise | context)
       ├─ Form submit state (pending, data, action)
       │  └─ useFormStatus / useActionState
       └─ Optimistic UI before server response
          └─ useOptimistic(state, updateFn)
    ```
    
    ## Component Pattern Decision Tree
    
    ```
    What's your composition challenge?
    │
    ├─ Group of related components sharing implicit state
    │  (Tabs, Accordion, Select, Menu)
    │  └─ Compound Components with Context
    │     Parent provides state via Context
    │     Children consume via useContext
    │
    ├─ Consumer needs to control rendering output
    │  └─ Render Props: children(props) or render={fn}
    │     Good for: headless UI, flexible layouts
    │
    ├─ Apply cross-cutting concerns (auth, logging, theming)
    │  to multiple components
    │  └─ Higher-Order Components (HOC)
    │     Wrap with withAuth(Component) or withLogging(Component)
    │     Prefer custom hooks for pure logic
    │
    ├─ Encapsulate reusable stateful logic
    │  └─ Custom Hook — always prefer over HOC when possible
    │     Composable, testable, no wrapper hell
    │
    ├─ Need imperative control from parent (focus, scroll, reset)
    │  └─ forwardRef + useImperativeHandle
    │
    ├─ Render content outside DOM hierarchy (modal, tooltip, toast)
    │  └─ Portal: createPortal(content, document.body)
    │
    ├─ Accept arbitrary children/slots without prop drilling
    │  └─ Slot pattern via children, or named props (header, footer)
    │
    └─ Polymorphic rendering (button that renders as <a> or div)
       └─ as prop pattern with TypeScript generics
    ```
    
    ## State Management Decision Tree
    
    ```
    Where does this state live and who owns it?
    │
    ├─ Only one component needs it
    │  └─ useState or useReducer (local state)
    │
    ├─ A few nearby components need it
    │  └─ Lift state to nearest common ancestor + prop drilling
    │     (2-3 levels is fine)
    │
    ├─ Many components need it, rarely changes
    │  (theme, locale, auth user)
    │  └─ React Context API
    │     Split contexts by update frequency
    │     Avoid single giant context
    │
    ├─ Global client state, changes often
    │  (shopping cart, UI preferences, navigation)
    │  ├─ Simple/small app → Zustand (minimal boilerplate)
    │  ├─ Atomic updates, React Suspense integration → Jotai
    │  └─ Large team, time-travel debugging, complex logic → Redux Toolkit
    │
    ├─ Server state (remote data, cache, sync)
    │  (API data, database queries)
    │  └─ TanStack Query (React Query)
    │     Handles: caching, background refetch, loading/error
    │     Don't use useState + useEffect for server data
    │
    └─ Form state
       └─ React Hook Form + Zod validation
          (controlled inputs are fine for simple forms)
    ```
    
    ## React 19 Quick Reference
    
    | Feature | API | Purpose |
    |---------|-----|---------|
    | `use()` hook | `use(promise)` / `use(context)` | Await promises in render, read context conditionally |
    | Actions | `async function action(formData)` | Async transitions with built-in pending state |
    | `useActionState` | `useActionState(action, initialState)` | Action result + pending state |
    | `useFormStatus` | `useFormStatus()` | Pending/data/method inside form |
    | `useOptimistic` | `useOptimistic(state, updateFn)` | Optimistic UI before server response |
    | React Compiler | Automatic memoization | Replaces most `memo`, `useMemo`, `useCallback` |
    | `ref` as prop | `<Input ref={ref}>` | No more forwardRef wrapper needed |
    | `<Context>` as provider | `<MyContext value={val}>` | No more `<MyContext.Provider>` |
    
    ```tsx
    // React 19: use() for data fetching in Server Components
    import { use } from 'react';
    
    function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
      const user = use(userPromise); // suspends until resolved
      return <h1>{user.name}</h1>;
    }
    
    // React 19: useActionState
    import { useActionState } from 'react';
    
    function ContactForm() {
      const [state, action, isPending] = useActionState(
        async (prevState: State, formData: FormData) => {
          const result = await submitContact(formData);
          return result;
        },
        { error: null }
      );
    
      return (
        <form action={action}>
          <input name="email" type="email" />
          <button disabled={isPending}>
            {isPending ? 'Sending...' : 'Send'}
          </button>
          {state.error && <p>{state.error}</p>}
        </form>
      );
    }
    ```
    
    ## Server vs Client Components
    
    ```
    Does this component need...?
    │
    ├─ useState, useReducer, useContext
    │  └─ Client Component ('use client')
    │
    ├─ useEffect, useLayoutEffect
    │  └─ Client Component ('use client')
    │
    ├─ Browser APIs (window, document, localStorage)
    │  └─ Client Component ('use client')
    │
    ├─ Event handlers (onClick, onChange, onSubmit)
    │  └─ Client Component ('use client')
    │
    ├─ Third-party libraries that use hooks/browser APIs
    │  └─ Client Component ('use client')
    │
    ├─ Direct database/file system access
    │  └─ Server Component (default, no directive)
    │
    ├─ Access to env vars (server-only secrets)
    │  └─ Server Component
    │
    ├─ Large dependencies you want to keep off the client bundle
    │  └─ Server Component
    │
    └─ async/await at the top level
       └─ Server Component
    ```
    
    **Client boundary rules:**
    - `'use client'` marks a boundary — everything imported below it becomes client JS
    - Server Components can import Client Components (they pass as props/children)
    - Client Components CANNOT import Server Components directly
    - Pass Server Component output as `children` prop to Client Components
    - Server data → Client: pass as serializable props only (no functions, classes, DOM nodes)
    
    ## Performance Checklist
    
    | Technique | When to Use | When NOT to Use |
    |-----------|-------------|-----------------|
    | `React.memo` | Component re-renders often with same props | Nearly everything — adds comparison overhead |
    | `useMemo` | Expensive calculation (>1ms), stable dep array | Primitive values, simple expressions |
    | `useCallback` | Callback passed to memoized child or in dep array | Inline handlers on DOM elements |
    | `React.lazy` + `Suspense` | Large components not needed on initial load | Small components, SSR-critical content |
    | `useTransition` | Non-urgent state updates (filtering, sorting) | Time-sensitive UI (typing, hover) |
    | `useDeferredValue` | Derived expensive render from fast-changing value | Same as above |
    | Virtualization | Lists >100 items | Small lists — overhead not worth it |
    | React Compiler (v19) | Automatic — replaces most manual memoization | Opt-out with `"use no memo"` if needed |
    
    ## Common Gotchas
    
    | Gotcha | Why It Happens | Fix |
    |--------|---------------|-----|
    | Stale closure in useEffect | Callback captures old state/prop at definition time | Add value to dep array, or use functional update `setState(prev => ...)` |
    | Missing useEffect dependency | Linter disabled or ignored, stale data shown | Never disable exhaustive-deps; use `useCallback` to stabilize functions |
    | Index as list key | Keys change on reorder/insert, causing wrong component identity | Use stable unique ID from data (`item.id`) |
    | Hydration mismatch | Server HTML doesn't match first client render | Avoid `typeof window`, random values, or dates in render; use `useEffect` for client-only content |
    | Unnecessary re-renders from context | All consumers re-render when any context value changes | Split context by concern; memoize context value with `useMemo` |
    | useEffect for derived state | State derived from another state causes extra render cycle | Compute derived value during render inline or with `useMemo` |
    | Missing cleanup in useEffect | Memory leaks from subscriptions, timers, fetch requests | Always return cleanup function; use AbortController for fetch |
    | Strict Mode double invocation | Effects run twice in dev to catch bugs | Design effects to be idempotent; cleanup must fully reverse effect |
    | Controlled/uncontrolled switch | `value` prop toggling between defined and `undefined` | Always provide defined value or always use `defaultValue`; never both |
    | Object/array in dep array | New reference every render triggers effect repeatedly | Memoize with `useMemo`; use primitive values in deps where possible |
    | Async function directly in useEffect | `useEffect(() => async () => {})` returns a Promise, not cleanup | Wrap: `useEffect(() => { async function run() {...}; run(); }, [])` |
    
    ## Reference Files
    
    | File | When to Load |
    |------|-------------|
    | `./references/hooks-patterns.md` | Deep hook usage: custom hooks, React 19 hooks, useEffect patterns, hook composition |
    | `./references/component-architecture.md` | Compound components, HOC, render props, portals, forwardRef, polymorphic components |
    | `./references/state-management.md` | Context API, Zustand, Jotai, Redux Toolkit, TanStack Query, React Hook Form |
    | `./references/server-components.md` | RSC architecture, Server Actions, Next.js App Router, caching, streaming, metadata |
    | `./references/performance.md` | React.memo, code splitting, virtualization, React Compiler, Web Vitals, profiling |
    | `./references/testing.md` | RTL queries, user-event, MSW, renderHook, Vitest setup, accessibility testing |
    
    ## Staleness Verifier
    
    This skill encodes fast-moving facts (the React 19 API surface, the ecosystem
    package stack). [`scripts/check-react-facts.py`](scripts/check-react-facts.py)
    guards them against silent drift — internal consistency in PR CI, live
    major-version drift in the scheduled freshness job:
    
    ```bash
    # Structural (PR CI, no network): every catalogued package + React 19 gate is
    # still named in this skill's prose, and the currency note still carries a year.
    python3 skills/react-ops/scripts/check-react-facts.py --offline        # exit 0 consistent, 10 drift
    
    # Live (weekly freshness job, never blocks a PR): is any documented major
    # now behind npm's latest dist-tag?
    python3 skills/react-ops/scripts/check-react-facts.py --live           # exit 10 a major moved ahead, 7 npm unreachable
    ```
    
    The canonical fact list lives in [`assets/react-facts.json`](assets/react-facts.json); when you add or drop a recommendation or the prose stops naming one, update it to match or `--offline` fails CI.
    
    ## See Also
    
    | Skill | When to Combine |
    |-------|----------------|
    | `typescript-ops` | TypeScript generics with React props, discriminated unions for state machines, utility types |
    | `testing-ops` | Test strategy, mocking patterns, CI integration, snapshot vs behavioral tests |
    | `tailwind-ops` | CSS-in-JS alternatives, responsive design with Tailwind in React components |
    | `javascript-ops` | Async patterns, Promises, generators, module system fundamentals |
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related