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.

ソース情報

リポジトリ
cosmix/loom
ソースの最終更新活動
2026年9月19日 08:01
検出された SKILL.md の言語
英語
スター
56
フォーク
3

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
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.
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る