frontend ยท diff
git:20261025.e435ca4 to git:20261025.8aa06bd
113 added, 21 removed. Audit A to A.
---
name: frontend
description: Frontend architecture, state management, and lifecycle engineering skill. Governs React 19, Next.js, and Flutter with clean boundary isolation.
---
# Frontend Architecture & State Management (`/frontend`)
- > **Philosophy**: *Keep components pure. Isolate side effects. Manage state deliberately. Handle errors at the boundary.*
+ > **Philosophy**: *Keep components pure. Isolate side effects. Manage state deliberately. Handle errors at the boundary. Never leak business logic into render trees.*
- This skill enforces high-performance client architecture across React 19, Next.js App Router, TypeScript, and Flutter.
+ This skill enforces high-performance client architecture across React 19, Next.js App Router, TypeScript, and modern web applications.
---
## 1. Architecture Tiers & Component Separation
```mermaid
flowchart TD
- A["Server / API Layer"] --> B["Data Access / Repository (TanStack Query / SWR)"]
- B --> C["Domain State & Hooks (useFeatureState)"]
- C --> D["Pure UI Presenters (Stateless Components)"]
- D --> E["User Interaction"]
- E --> C
+ subgraph Server["Server Layer"]
+ A["React Server Component (RSC)"]
+ B["Server Actions (Zod-validated)"]
+ end
+ subgraph ClientData["Client Data & State"]
+ C["TanStack Query / SWR / URL State"]
+ D["Optimistic State (useOptimistic)"]
+ end
+ subgraph UI["Pure Presentation"]
+ E["Stateless Presentational Components"]
+ F["Error Boundary & Suspense Skeletons"]
+ end
+ A --> C
+ B --> D
+ C --> D
+ D --> E
+ E --> F
```
- ### Rule 1: Clean Boundary Separation
- - **No Direct Fetching in Presentation**: Never trigger inline `fetch()` or `axios.get()` inside JSX render bodies. Encapsulate data fetching within dedicated query hooks or server components.
- - **Server Components by Default**: In Next.js App Router, keep components as React Server Components (RSC) unless interactive hooks (`useState`, `useEffect`, `onClick`) are required. Place `"use client"` strictly at leaf nodes.
+ ### 1.1 Clean Boundary Separation Rules
+ 1. **Server Components by Default**: In Next.js App Router, all components are React Server Components (RSC) unless interactive hooks (`useState`, `useEffect`, `useRef`, browser events) are strictly required.
+ 2. **Push `"use client"` to the Leaves**: Never mark an entire page or large container `"use client"`. Wrap only the interactive controls (buttons, inputs, dropdowns) at the leaf nodes.
+ 3. **Zero Inline Fetches in JSX**: Never trigger raw HTTP requests directly inside presentation components. Use dedicated hooks or server functions.
- ### Rule 2: Memory Safety & Effect Cleanup
- - Always wire an `AbortController` or cancellation signal to async operations inside `useEffect`:
+ ---
+
+ ## 2. State Management Decision Matrix
+
+ Choose the simplest state layer appropriate for the data lifecycle:
+
+ | State Scope | Recommended Tool | Example Use Case | Anti-Pattern |
+ | :--- | :--- | :--- | :--- |
+ | **URL State** | `useSearchParams` / `nuqs` | Filters, pagination, active tab, modal open status | Duplicating URL search params into local `useState` |
+ | **Server Cache** | `TanStack Query` or RSC | API data, entity collections, cached user profiles | Storing API responses in Redux/Zustand without cache invalidation |
+ | **Interactive Form** | `React 19 Actions` + `useActionState` | Form submission, password updates, checkout flow | Managing 15 separate `useState` variables for inputs |
+ | **Client UI Global** | `Zustand` (minimal stores) | Theme preference, audio player state, active sidebar drawer | Putting massive complex database entities into global client store |
+ | **Local Ephemeral** | `useState` / `useReducer` | Dropdown open/close, hover tooltip, slider value | Passing down 5 levels of props instead of local state |
+
+ ---
+
+ ## 3. React 19 Server Actions & Optimistic Updates
+
+ Standardize form mutations using React 19 Actions and `useOptimistic`:
+
+ ```tsx
+ "use client";
+
+ import { useActionState, useOptimistic, startTransition } from "react";
+ import { updateProfile } from "@/actions/profile";
+ import { UserCheck } from "lucide-react";
+
+ interface ProfileFormProps {
+ initialName: string;
+ }
+
+ export function ProfileForm({ initialName }: ProfileFormProps) {
+ const [state, formAction, isPending] = useActionState(updateProfile, { success: true, error: null });
+ const [optimisticName, setOptimisticName] = useOptimistic(
+ initialName,
+ (current, newName: string) => newName
+ );
+
+ const handleSubmit = (formData: FormData) => {
+ const nextName = formData.get("name") as string;
+ startTransition(() => {
+ setOptimisticName(nextName);
+ });
+ formAction(formData);
+ };
+
+ return (
+ <form action={handleSubmit} className="flex flex-col gap-3 max-w-sm">
+ <label htmlFor="name-input" className="text-xs font-medium text-zinc-300">
+ Display Name (Current: {optimisticName})
+ </label>
+ <input
+ id="name-input"
+ name="name"
+ defaultValue={initialName}
+ disabled={isPending}
+ className="px-3 py-1.5 bg-zinc-900 border border-zinc-800 rounded text-sm text-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-400"
+ />
+ {state.error && <p className="text-xs text-rose-400">{state.error}</p>}
+ <button
+ type="submit"
+ disabled={isPending}
+ className="inline-flex items-center justify-center gap-2 px-3 py-1.5 bg-zinc-100 hover:bg-zinc-200 text-zinc-900 text-xs font-semibold rounded transition-colors disabled:opacity-50"
+ >
+ <UserCheck className="w-3.5 h-3.5" aria-hidden="true" />
+ {isPending ? "Saving..." : "Save Changes"}
+ </button>
+ </form>
+ );
+ }
+ ```
+
+ ---
+
+ ## 4. Memory Safety & Effect Cleanup
+
+ Always wire an `AbortController` or cancellation signal to async operations inside `useEffect`:
+
```typescript
useEffect(() => {
const controller = new AbortController();
+ let isMounted = true;
+
async function loadData() {
try {
const res = await fetch("/api/v1/metrics", { signal: controller.signal });
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
- setMetrics(data);
+ if (isMounted) setMetrics(data);
} catch (err: unknown) {
- if ((err as Error).name !== "AbortError") {
+ if ((err as Error).name !== "AbortError" && isMounted) {
setError(err instanceof Error ? err.message : "Fetch failed");
}
}
}
+
loadData();
- return () => controller.abort();
+ return () => {
+ isMounted = false;
+ controller.abort();
+ };
}, []);
```
- ### Rule 3: Error Boundaries & Fallback States
- - Every route or primary feature widget must be wrapped in an `ErrorBoundary`.
- - Display actionable, human-readable error messages with retry actions rather than unhandled white screens.
-
---
- ## 2. Frontend Architecture Validator CLI
+ ## 5. Frontend Architecture Validator CLI
- Scan frontend modules for architecture violations:
+ Scan frontend modules for architecture violations, inline fetches in JSX, and missing cleanup:
```bash
python3 skills/frontend/scripts/verify_frontend_architecture.py --path .
```