Skip to main content

loom-react

Modern client-side React development patterns. Not for: a loom-hooks shell script or a stage's lifecycle state.

Informations de source

Dépôt
cosmix/loom
Dernière activité de la source
19 septembre 2026 à 08:01
Langue détectée de SKILL.md
anglais
Étoiles
61
Forks
4

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
loom-react
description
Modern client-side React development patterns. Not for: a loom-hooks shell script or a stage's lifecycle state.
triggers
["react","jsx","tsx","component","react hook","useState","useEffect","useContext","useReducer","useMemo","useCallback","useRef","props","render","virtual DOM","reconciliation","single page application","spa","react-router","jotai","vite","bun","oxc","oxlint","oxfmt","rolldown","react compiler","eslint","prettier","client-side routing","accessibility","a11y","[Truncated]"]
# React SPA Development ## Overview Client-side React 19+ SPAs. Stack: **React Router v7** (routing/loaders), **Jotai** (atomic global state), **Vite** (build — Rolldown bundler, Oxc transforms and minifier, Lightning CSS), **oxlint** + **oxfmt** (lint/format — the Oxc replacements for ESLint and Prettier), **Bun** (package manager/runtime). NOT for SSR frameworks (Next.js/Remix) — those are out of scope. The single densest section is **Expert Practices** at the end — read it first if you know React basics. The middle sections are reference implementations. ## React 19 Features ### Actions and useActionState React 19 introduces Actions for handling async state transitions: ```typescript import { useActionState } from 'react' interface FormState { message: string error?: string } async function updateProfile(previousState: FormState, formData: FormData) { const name = formData.get('name') as string try { await fetch('/api/profile', { method: 'POST', body: JSON.stringify({ name }), }) return { message: 'Profile updated successfully' } } catch (error) { return { message: '', error: 'Update failed' } } } export function ProfileForm() { const [state, formAction, isPending] = useActionState(updateProfile, { message: '' }) return ( <form action={formAction}> <input type="text" name="name" disabled={isPending} /> <button type="submit" disabled={isPending}> {isPending ? 'Updating...' : 'Update Profile'} </button> {state.error && <p className="error">{state.error}</p>} {state.message && <p className="success">{state.message}</p>} </form> ) } ``` ### useOptimistic for Instant UI Updates ```typescript import { useOptimistic, useState } from 'react' interface Todo { id: string title: string completed: boolean } export function TodoList({ todos }: { todos: Todo[] }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic( todos, (state, newTodo: Todo) => [...state, newTodo] ) async function addTodo(formData: FormData) { const title = formData.get('title') as string const tempTodo = { id: crypto.randomUUID(), title, completed: false } addOptimisticTodo(tempTodo) await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ title }), }) } return ( <div> <ul> {optimisticTodos.map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> <form action={addTodo}> <input type="text" name="title" /> <button type="submit">Add Todo</button> </form> </div> ) } ``` ### use() for Reading Promises and Context ```typescript import { use, Suspense } from 'react' import { useLoaderData } from 'react-router' interface User { id: string name: string } function UserProfile({ userPromise }: { userPromise: Promise<User> }) { // use() unwraps the promise. It cannot be wrapped in try/catch — // a rejected promise surfaces at the nearest Error Boundary. const user = use(userPromise) return <div>{user.name}</div> } // CRITICAL: the Promise must be created OUTSIDE the render cycle. Promises // created in client components are recreated on every render, so passing an // inline fetchUser(userId) to use() re-suspends and re-fetches forever. // In this React Router v7 SPA stack a route loader is the idiomatic stable // source (one promise per navigation). export function UserContainer() { const userPromise = useLoaderData() as Promise<User> return ( <Suspense fallback={<div>Loading user...</div>}> <UserProfile userPromise={userPromise} /> </Suspense> ) } ``` ### Document Metadata React 19 hoists `<title>`/`<meta>`/`<link>` rendered anywhere in the tree into `<head>` — no helper library needed. Just render them inside the component (e.g. a route page). **Server/Client Components:** N/A here — a pure SPA has no server, so every component is a client component with full access to browser APIs, hooks, and event handlers. `'use client'`/RSC only matter under Next.js/Remix (out of scope). ## UI Component Patterns ### Design System Foundation ```typescript // src/components/ui/Button.tsx import { ComponentPropsWithoutRef } from 'react' import { cva, type VariantProps } from 'class-variance-authority' const buttonVariants = cva( 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { default: 'bg-primary text-white hover:bg-primary/90', secondary: 'bg-secondary text-white hover:bg-secondary/90', outline: 'border border-gray-300 bg-transparent hover:bg-gray-100', ghost: 'hover:bg-gray-100', danger: 'bg-red-600 text-white hover:bg-red-700', }, size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4', lg: 'h-12 px-6 text-lg', icon: 'h-10 w-10', }, }, defaultVariants: { variant: 'default', size: 'md', }, } ) export interface ButtonProps extends ComponentPropsWithoutRef<'button'>, VariantProps<typeof buttonVariants> { isLoading?: boolean ref?: React.Ref<HTMLButtonElement> } // React 19: ref is a plain prop — no forwardRef wrapper, no displayName needed. export function Button({ ref, className, variant, size, isLoading, children, ...props }: ButtonProps) { return ( <button ref={ref} className={buttonVariants({ variant, size, className })} disabled={isLoading || props.disabled} {...props} > {isLoading ? ( <> <svg className="animate-spin -ml-1 mr-2 h-4 w-4" /> Loading... </> ) : ( children )} </button> ) } ``` ### Composition (Card slots) Ship a family of thin primitives that forward `ref` (a plain prop in React 19) and `className`. Callers compose them; no prop explosion. ```typescript type DivProps = ComponentPropsWithoutRef<'div'> & { ref?: React.Ref<HTMLDivElement> } export function Card({ ref, className, ...props }: DivProps) { return <div ref={ref} className={`rounded-lg border bg-white shadow-sm ${className}`} {...props} /> } export function CardHeader({ ref, className, ...props }: DivProps) { return <div ref={ref} className={`p-6 ${className}`} {...props} /> } export function CardContent({ ref, className, ...props }: DivProps) { return <div ref={ref} className={`p-6 pt-0 ${className}`} {...props} /> } // <Card><CardHeader>…</CardHeader><CardContent>…</CardContent></Card> ``` ### Polymorphic Components ```typescript // src/components/ui/Text.tsx import { ElementType, ComponentPropsWithoutRef } from 'react' type TextProps<E extends ElementType> = { as?: E variant?: 'h1' | 'h2' | 'h3' | 'body' | 'small' } & ComponentPropsWithoutRef<E> export function Text<E extends ElementType = 'p'>({ as, variant = 'body', className, ...props }: TextProps<E>) { const Component = as || 'p' const variantClasses = { h1: 'text-4xl font-bold', h2: 'text-3xl font-semibold', h3: 'text-2xl font-semibold', body: 'text-base', small: 'text-sm text-gray-600', } return ( <Component className={`${variantClasses[variant]} ${className || ''}`} {...props} /> ) } // Usage - flexible element types <Text variant="h1">Heading</Text> <Text as="h1" variant="h1">Heading with h1 tag</Text> <Text as="span" variant="small">Small text in span</Text> ``` ### Render Props / Function-as-Children Generic state-branching component. `children: (data: T) => ReactNode`. In this stack prefer Suspense + async atoms/loaders for data; render props remain useful for non-Suspense state machines. ```typescript export function DataLoader<T>({ data, isLoading, error, children }: { data: T | null; isLoading: boolean; error: Error | null; children: (data: T) => ReactNode }) { if (isLoading) return <div>Loading…</div> if (error) return <div>Error: {error.message}</div> if (!data) return null return <>{children(data)}</> } // <DataLoader {...useFetch<User[]>('/api/users')}>{(u) => …}</DataLoader> ``` ## Project Setup ### Initial Setup with Bun and Vite ```bash # Create new React app with Vite template (Vite runs on Rolldown + Oxc) bun create vite my-app --template react-ts cd my-app # Install dependencies bun install # Add React Router and Jotai bun add react-router jotai # Add development dependencies: types, oxlint (lint), oxfmt (format) bun add -D @types/react @types/react-dom oxlint oxfmt bunx oxlint --init # .oxlintrc.json — then enable the React plugins (next section) bunx oxfmt --init # .oxfmtrc.json # If the template scaffolded ESLint, remove it — oxlint replaces it: # delete eslint.config.js and `bun remove` every eslint*/typescript-eslint devDependency # Start development server bun run dev ``` ### Lint and Format (oxlint + oxfmt) oxlint and oxfmt are the Oxc-native replacements for ESLint and Prettier — same job, no plugin graph, no linter/formatter conflict config. Setting `plugins` in `.oxlintrc.json` REPLACES the default set (`eslint`, `typescript`, `unicorn`, `oxc`), so list the defaults you keep next to the React ones. Only the `correctness` category is on by default: `react/rules-of-hooks` is `pedantic`, so it stays off until you name it, and `react/exhaustive-deps` is `correctness` but only fires once `react` is in `plugins` — list both. ```json { "$schema": "./node_modules/oxlint/configuration_schema.json", "plugins": ["eslint", "typescript", "unicorn", "oxc", "react", "jsx-a11y", "import", "vitest"], "categories": { "correctness": "error", "suspicious": "warn" }, "rules": { "react/rules-of-hooks": "error", "react/exhaustive-deps": "error", "react/jsx-key": "error", "typescript/no-explicit-any": "error" }, "env": { "browser": true }, "ignorePatterns": ["dist/**", "coverage/**"] } ``` `package.json` scripts: ```json { "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build", "preview": "vite preview", "test": "vitest run", "typecheck": "tsc --noEmit", "lint": "oxlint --deny-warnings", "lint:fix": "oxlint --fix", "format": "oxfmt", "format:check": "oxfmt --check", "check": "bun run typecheck && bun run lint && bun run format:check && bun run test" } } ``` - `oxfmt` is Prettier-compatible: `.oxfmtrc.json`, honors `.gitignore`/`.prettierignore`, default `printWidth` is 100 where Prettier's is 80 (`oxfmt --migrate=prettier` carries an existing Prettier config over). `oxfmt` writes in place; `oxfmt --check` gates CI. - `oxlint --fix` applies safe fixes only; `--fix-suggestions`/`--fix-dangerously` may change behavior — review the diff. - Leave the `react-perf` plugin off while the React Compiler is on: its rules flag the inline handlers and object props the compiler memoizes for you, and under `--deny-warnings` they fail the build.
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub