--- name: preact-ui description: Use when designing, refactoring, or reviewing Preact pages and components, including component structure, MVVM architecture, signals, hooks, forms, routing, data fetching, accessibility, SCSS integration, and tests. --- # Preact UI Skill Use this skill when building Preact pages, component systems, or full applications. Prefer the **MVVM (Model–View–ViewModel)** pattern to cleanly separate business logic from rendering. ## Architecture: MVVM in Preact Organize code into three layers: - **Model** — pure TypeScript modules for domain data, validation rules, and API contracts. No Preact imports. - **ViewModel** — custom hooks (`useXxxViewModel`) or module-level signals that hold state, computed values, and commands. Bridges Models to Views. - **View** — functional components that only render and call ViewModel hooks. No business logic inside. Directory layout: ``` src/ models/ # domain types, validation, factories viewmodels/ # useXxxViewModel hooks, signal stores components/ # shared presentational UI atoms/molecules features/ # feature slices (view + viewmodel colocated) routes/ # route-level page components services/ # API clients, storage adapters hooks/ # generic reusable hooks styles/ # global SCSS, tokens, mixins ``` ## Core approach 1. Define the Model first — data shape, validation, and invariants. 2. Build the ViewModel — expose signals, computed values, and actions. 3. Build the View last — render only, bind to the ViewModel. 4. Keep components small and composable. 5. Treat accessibility and responsiveness as design constraints, not afterthoughts. ## Preact fundamentals - Import from `preact` and `preact/hooks`, not React. - Use `@preact/signals` for reactive state — prefer over `useState` for shared or performance-sensitive state. - Use `preact/compat` only when consuming React-targeting third-party libraries. - Use `preact-iso` (the official meta-framework router) or `preact-router` for client-side routing. - Scaffold new projects with `npm create preact@latest` — it sets up Vite, TypeScript, and preact-iso. ## Signals (preferred state primitive) - `signal(value)` — reactive value; read `.value`, write `.value =`. - `computed(() => expr)` — derived signal; recalculates only when dependencies change. - `effect(() => sideEffect)` — runs when dependencies change; call the returned cleanup to dispose. - `useSignal(value)` — component-scoped signal; behaves like `useState` but with signal semantics. - `useComputed(() => expr)` — component-scoped computed signal. - Pass signals directly into JSX: `{count}` updates the DOM node without re-rendering the component. - Define shared signals in a ViewModel module; import them in View components. - Use `batch(() => { ... })` to group multiple signal updates into a single render. ## Hooks and state - Use `useState` / `useSignal` for purely local UI state (toggle, hover, open). - Use `useEffect` for external subscriptions, timers, and DOM side-effects; always return cleanup. - Use `useRef` for DOM references and mutable values that should not trigger re-renders. - Use `useMemo` / `useComputed` for expensive derived computations. - Use `useCallback` only when passing stable references to memoized children. - Avoid `useEffect` for data that can be modelled as derived/computed state. ## Workflow 1. Identify the page role: - marketing / landing page - dashboard with live data - form flow (wizard, multi-step) - content / article page - interactive tool or widget 2. Define the layout hierarchy before writing any component code. 3. Separate presentational and data-fetching concerns. 4. Model state intentionally: - local signal/state for local UI - module-level signals for cross-component shared state - `computed` for derived values — never duplicate state 5. Wire routing at the route layer; keep route components thin. 6. Integrate SCSS through predictable class names and component-scoped partials. 7. Add accessibility checks for keyboard, labels, focus order, and semantics. 8. Write tests for user-visible behavior, not implementation details. ## Component rules - One main concern per component. - Props describe intent, not DOM mechanics. - Use semantic HTML first; add ARIA only when semantics are insufficient. - Avoid overusing Context when signals or props are simpler. - Never put business logic inside a View component — move it to the ViewModel. - Memoize with `memo()` only when profiling identifies a bottleneck. ## Forms - Use controlled inputs when validation or live feedback is needed; use signals for field values. - Use uncontrolled inputs (`useRef`) for simple cases without validation. - Expose clear inline validation messages tied to each field. - Keep submit, pending, and error states visible and distinct. - Preserve keyboard focus after submit or error. - Centralize form validation rules in the Model layer. ## Data fetching - Define API calls in `src/services/` — return typed results, never raw `fetch` in components. - Expose loading, data, and error signals from the ViewModel. - Handle retry and cancellation in the service layer (use `AbortController`). - Keep transport details (headers, base URL, serialization) out of components. - Use `useEffect` with cleanup for subscriptions; prefer signal-driven fetch triggers. ## Routing (preact-iso) - Use `` and `` from `preact-iso` for declarative routing. - Use lazy loading (`lazy()` + `Suspense`) for route-level code splitting. - Keep route components thin — they delegate to feature-level Views. - Read route params via the `useRoute()` hook; never parse `window.location` manually. - Handle 404 with a catch-all `` component. ## SCSS integration - Use a stable BEM-like class naming scheme aligned with the component name. - Pair each component with a focused `_component.scss` partial. - Reuse design tokens, mixins, and spacing scales from the global SCSS system. - Define responsive breakpoints and theme variants at the token level, not inline. - Keep dark/light theme support via CSS custom properties toggled at the root. ## Accessibility - Use semantic landmarks (`
`, `