plugins/languages/typescript/skills/react/SKILL.md
TypeScript / JavaScript React 开发规范,覆盖 React 19 Server Components / Server Actions / use() hook / useActionState / useOptimistic / useFormStatus / Suspense、Next.js 15 App Router、Route Handlers、React Compiler 自动 memo、TanStack Query 5、TanStack Router、Zustand 状态、自定义 hook AbortController 模板。Use when 开发 React 组件、页面路由、SSR、状态管理、表单处理、数据获取,或用户提到 "React"、"Next.js"、"Server Components"、"use client"、"Server Actions"、"useState"、"JSX"、"App Router"。
npx skillsauth add lazygophers/ccplugin typescript-reactInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
本 skill 同时覆盖 JavaScript 项目;示例以 TS 为主,JS 项目去掉类型即可。
React 19 默认 Server Components;"use client" 是边界标记,不滥用。
typescript-core — 工具链与基线typescript-async — AbortController, Suspense 配合typescript-security — XSS, dangerouslySetInnerHTMLReact 19 Compiler 自动记忆化组件、hooks、JSX,手写 useMemo / useCallback / memo 改为可选。仅在 profiler 证实瓶颈时手动优化。
// ❌ React 18 手写 (Compiler 启用后冗余)
const v = useMemo(() => compute(data), [data]);
const onClick = useCallback(() => f(id), [id]);
export default React.memo(MyComp);
// ✅ React 19 + Compiler
const v = compute(data);
const onClick = () => f(id);
export default MyComp;
// app/users/page.tsx
import { db } from "@/lib/db";
export default async function UsersPage() {
const users = await db.user.findMany();
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
}
// app/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.email(),
});
export async function createUser(_: unknown, formData: FormData) {
const r = CreateUserSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
});
if (!r.success) return { errors: z.flattenError(r.error).fieldErrors };
await db.user.create({ data: r.data });
revalidatePath("/users");
return { ok: true as const };
}
import { use, Suspense } from "react";
function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspend
return <div>{user.name}</div>;
}
<Suspense fallback={<Loading />}>
<UserProfile userPromise={fetchUser(id)} />
</Suspense>
"use client";
import { useActionState, useOptimistic } from "react";
import { useFormStatus } from "react-dom";
function CreateUserForm() {
const [state, action, isPending] = useActionState(createUser, null);
return (
<form action={action}>
<input name="name" required />
<input name="email" type="email" required />
{state?.errors && <p>{JSON.stringify(state.errors)}</p>}
<SubmitButton />
</form>
);
}
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "..." : "Create"}</button>;
}
// 乐观更新
const [optimistic, addOptimistic] = useOptimistic(
messages,
(prev, next) => [...prev, { ...next, sending: true }],
);
// document metadata 原生
<title>{post.title}</title>
<meta name="description" content={post.excerpt} />
// ✅ 普通函数
type UserCardProps = {
user: User;
onSelect?: (id: string) => void;
};
export function UserCard({ user, onSelect }: UserCardProps) {
return (
<div onClick={() => onSelect?.(user.id)}>
<h3>{user.name}</h3>
</div>
);
}
// ❌ 禁 React.FC (隐式 children、泛型不友好)
export function useFetch<T>(url: string) {
const [state, setState] = useState<AsyncState<T>>({ status: "idle" });
useEffect(() => {
const ctrl = new AbortController();
setState({ status: "loading" });
fetch(url, { signal: ctrl.signal })
.then((r) => r.json() as Promise<T>)
.then((data) => setState({ status: "success", data }))
.catch((error: unknown) => {
if (!ctrl.signal.aborted) setState({ status: "error", error: error as Error });
});
return () => ctrl.abort();
}, [url]);
return state;
}
| 场景 | 推荐 |
|------|------|
| Next.js App Router | Server Components + fetch() + revalidate |
| Vite SPA | TanStack Query 5 (useSuspenseQuery + Suspense) |
| Realtime / 订阅 | WebSocket / SSE in useEffect + AbortController |
| 表单 | Server Actions / TanStack Form |
// TanStack Query 5 + Suspense
import { useSuspenseQuery } from '@tanstack/react-query';
function User({ id }: { id: string }) {
const { data } = useSuspenseQuery({
queryKey: ['user', id],
queryFn: ({ signal }) => fetch(`/api/users/${id}`, { signal }).then(r => r.json()),
});
return <div>{data.name}</div>;
}
// app/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = { title: "App", description: "..." };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html lang="en"><body>{children}</body></html>;
}
// app/api/users/route.ts
import { NextResponse } from "next/server";
export async function POST(request: Request) {
const body: unknown = await request.json();
const r = CreateUserSchema.safeParse(body);
if (!r.success) {
return NextResponse.json({ errors: z.flattenError(r.error) }, { status: 400 });
}
const user = await db.user.create({ data: r.data });
return NextResponse.json(user, { status: 201 });
}
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./pages/Dashboard'));
<Suspense fallback={<Spinner />}><Dashboard /></Suspense>
useState / useReduceruseSearchParams, TanStack Router: typed)所有 React 19 API 在 JS 项目同样可用,去掉类型注解即可。JS 项目用 JSDoc 标注 props:
/**
* @param {{ user: User; onSelect?: (id: string) => void }} props
*/
export function UserCard({ user, onSelect }) {
return <div onClick={() => onSelect?.(user.id)}><h3>{user.name}</h3></div>;
}
| 现象 | 问题 | 严重 |
|------|------|------|
| React.FC | 隐式 children、泛型受限 | 中 |
| class 组件 | 函数组件 + Hooks | 高 |
| useEffect 内 fetch (无 abort) | TanStack Query 或加 AbortController | 中 |
| "use client" 顶层滥用 | 应最小化 client 边界 | 中 |
| 无 Suspense 边界 | 异步组件需 fallback | 高 |
| Server Actions 无 Zod | 服务端必须验证 | 高 |
| 手写 memo (Compiler 已开) | 冗余 | 低 |
| 无 key / index as key | 稳定 ID 作 key | 高 |
| 依赖数组缺失 | Biome/ESLint react-hooks 修复 | 高 |
| dangerouslySetInnerHTML 无清理 | DOMPurify | 高 |
React.FC)"use client" 仅边界react-compiler babel plugin)useActionStatetools
--- name: trellisx-workspace description: 维护 `.trellis/task.md` 任务看板 —— trellis 缺的跨任务总览。**一个表格, 一行一个任务**, 列为 id/名称/描述/状态/阶段/进度/worktree (状态/阶段中文显示)。在 task create/start/阶段切换/archive 后**及时更新**对应行; 并**自动清理超 7 天的已完成行**防膨胀。保持看板与 task.json 实时一致。 when_to_use: 维护 / 创建 / 更新 `.trellis/task.md` 任务看板时; task 生命周期任一节点 (create/start/阶段推进/archive) 之后同步看板时; 用户问"当前有哪些任务 / 任务进度 / 任务看板"时。被 trellisx-flow 与 trellisx-apply 注入的流程引用。 user-invocable: true argument-hint: [show|update|sync|cleanup ...] [task id] arguments:
testing
强制以 Trellis task 闭环处理用户指定的请求 (自判新建/并入 → plan→exec→check→finish 全程不跳步)。**仅用户显式主动调用** (/trellisx-flow 或明确要求"强制走 task 处理这个"); **禁止自动 / 被动 / 推断式调用** —— 不要因为某个请求"看起来该建 task"就自动触发本 skill, 那是 apply 注入的 no_task 倾向的职责。
testing
把 强推task + subtask拆分 + worktree隔离 + 闭环收尾 四维度增量注入当前项目 .trellis/ (workflow.md 的 no_task/planning/in_progress 块 + spec 背书文档 + trellis 生命周期 hook worktree 自动化)。强推 task 与闭环为纯 prompt 软约束 (非平台 hook 硬拦截)。**纯增量追加, 绝不替换 trellis 原生文本** (no_task 分类+征同意/check/finish/前缀全保留)。幂等 (marker 包裹)。
development
Claude Code 会话历史整理 — 扫 ~/.claude/projects/**/*.jsonl 全部 session transcripts, 提取学习增量 (用户校正/决策/踩坑/L0 规则) → 全局记忆库 ~/.cortex/.wiki/memory/. 默认 --apply 落盘 (--dry-run opt-in 仅出 JSON plan 预览). 与 cortex-extract (L4-inbox 内部) 互补.