一键导入
react-expert
Use this skill when you review or write React components (.tsx/.jsx) and need React 19 best practices (especially eliminating unnecessary useEffect).
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use this skill when you review or write React components (.tsx/.jsx) and need React 19 best practices (especially eliminating unnecessary useEffect).
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use this skill when you need to diagnose and fix React 19 performance issues (render churn, slow updates, Suspense/Transitions, memoization, profiling, and React Compiler).
Write clean, behavior-focused unit/integration tests that remain trustworthy when code is written or modified by agents (prevents “test weakening”, enforces invariants, and adds CI guardrails).
Use this skill when you are creating or updating Agent Skills in your project (structure, YAML frontmatter, naming, and progressive disclosure).
Use this skill when designing or refactoring boundaries, responsibilities, and change drivers using practical software architecture “hard parts” thinking + SRP.
Use this skill when you need a Bun runbook (dev, build, lint, typecheck, tests, migrations, and verification steps).
Use this skill when you need a thorough code hygiene pass (dead code, lint escapes, consistency, formatting drift, and subtle smells) on touched files.
| name | react-expert |
| description | Use this skill when you review or write React components (.tsx/.jsx) and need React 19 best practices (especially eliminating unnecessary useEffect). |
| metadata | {"applyTo":"**/*.tsx, **/*.jsx"} |
You are a React 19 expert who hates unnecessary useEffect. Your mission is to educate developers that most useEffect usage is a code smell and guide them toward proper React patterns.
react-hooks/exhaustive-deps). If deps feel “wrong”, refactor the code (split Effects, move objects/functions, use updater functions).
'use client' leaf-ish and pass only serializable props across Server → Client.
useEffect is an escape hatch, not a tool.
React 19 provides better primitives for almost everything developers historically used useEffect for. If you see useEffect, your first instinct should be: "How do I eliminate this?"
In this Next.js 16 server-first app, “avoiding effects” is also a cost/perf strategy:
For Vercel-specific cost guidance: .github/skills/nextjs-cost-performance/SKILL.md
Before writing useEffect, ask these questions in order:
// ❌ BAD: useEffect to "sync" derived state
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
// ✅ GOOD: Just compute it
const fullName = `${firstName} ${lastName}`;
Rule: If you're setting state based on other state/props, you don't need state OR effect.
// ❌ BAD: useEffect for data fetching in client
useEffect(() => {
fetch("/api/users")
.then((res) => res.json())
.then(setUsers);
}, []);
// ✅ GOOD: Server Component fetches, passes data down
// In page.tsx (Server Component)
const users = await db.query.users.findMany({
where: eq(users.workspaceId, workspaceId),
});
return <UserList users={users} />;
Rule: In Next.js 16, data lives in Server Components. Clients receive data as props.
// ❌ BAD: useEffect + fetch for mutations
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (shouldSubmit) {
fetch("/api/submit", { method: "POST", body: JSON.stringify(data) });
}
}, [shouldSubmit, data]);
// ✅ GOOD: Server Action with form
async function createClient(formData: FormData) {
"use server";
// validate, mutate, revalidate
}
<form action={createClient}>
<input name="name" />
<SubmitButton />
</form>;
Rule: Mutations belong in Server Actions. Use useActionState for pending/error states.
// ❌ BAD: useEffect to react to state change from user action
useEffect(() => {
if (selectedId) {
analytics.track("item_selected", { id: selectedId });
}
}, [selectedId]);
// ✅ GOOD: Track in the event handler that caused the change
function handleSelect(id: string) {
setSelectedId(id);
analytics.track("item_selected", { id });
}
Rule: If a user action caused it, handle the side effect in that action's handler.
If you need to read the latest state/props inside an Effect without re-running it, use Effect Events (
useEffectEvent) rather than lying to the dependency array.
- Constraints: Effect Events should only be called from within Effects; don’t pass them to other components/hooks.
- Reference: https://react.dev/reference/react/useEffectEvent
// ❌ BAD: useEffect to initialize from localStorage
const [theme, setTheme] = useState("light");
useEffect(() => {
setTheme(localStorage.getItem("theme") || "light");
}, []);
// ✅ GOOD: Lazy initializer (runs once, no hydration mismatch risk with SSR)
const [theme, setTheme] = useState(() => {
if (typeof window === "undefined") return "light";
return localStorage.getItem("theme") || "light";
});
// ❌ BAD: useEffect for subscriptions
useEffect(() => {
const unsubscribe = store.subscribe(() => setData(store.getState()));
return unsubscribe;
}, []);
// ✅ GOOD: useSyncExternalStore
const data = useSyncExternalStore(store.subscribe, store.getState);
// ❌ BAD: useEffect to manage optimistic state
useEffect(() => {
if (pendingItem) {
setItems((prev) => [...prev, pendingItem]);
}
}, [pendingItem]);
// ✅ GOOD: useOptimistic
const [optimisticItems, addOptimisticItem] = useOptimistic(
items,
(state, newItem) => [...state, newItem]
);
// ❌ BAD: useEffect to debounce search
useEffect(() => {
const timer = setTimeout(() => setDebouncedQuery(query), 300);
return () => clearTimeout(timer);
}, [query]);
// ✅ GOOD: useDeferredValue (React handles the timing)
const deferredQuery = useDeferredValue(query);
// ❌ BAD: useEffect for focus
useEffect(() => {
inputRef.current?.focus();
}, [shouldFocus]);
// ✅ GOOD: Ref callback (React 19)
<input
ref={(node) => {
if (node && shouldFocus) node.focus();
}}
/>;
Legitimate useEffect uses:
Even then, extract to a custom hook with a clear name.
| Pattern | Why it's wrong | Fix |
|---|---|---|
useEffect + setState with same deps | Creating render loops | Derive the value |
useEffect + fetch | Should be Server Component | Move fetch to server |
useEffect with empty deps for data | Race conditions, no Suspense | Server Component or React Query |
useEffect to "reset" state on prop change | Derived state antipattern | Use key prop or derive |
useEffect + setInterval without cleanup | Memory leak | Add cleanup, or question if needed |
| Pattern | Question to ask |
|---|---|
Any useEffect in a component | "Can I delete this?" |
useEffect with [prop] dependency | "Should this be an event handler?" |
Multiple useEffect in one component | "Is this component doing too much?" |
useEffect that sets state | "Can I derive this instead?" |
useEffect with object/array deps | "Am I causing infinite loops?" |
react-hooks/exhaustive-deps to “make it work”. This is a correctness bug waiting to happen. Refactor the code instead.When reviewing .tsx files, check each useEffect against:
useSyncExternalStoreuseOptimisticuseDeferredValue or useTransition// Server Action + useActionState
const [state, formAction, isPending] = useActionState(
createInvoice,
initialState
);
<form action={formAction}>
<Input name="clientId" />
{state.error && <p className="text-destructive">{state.error}</p>}
<Button disabled={isPending}>
{isPending ? "Creating..." : "Create Invoice"}
</Button>
</form>;
const [optimisticItems, addOptimistic] = useOptimistic(
items,
(state, newItem: Item) => [...state, { ...newItem, pending: true }]
);
async function handleAdd(formData: FormData) {
const newItem = Object.fromEntries(formData);
addOptimistic(newItem as Item);
await addItem(formData); // Server Action
}
// Use the `use` hook with Suspense
function ClientData({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise); // Suspends until resolved
return <div>{data.name}</div>;
}
// Wrapped in Suspense by parent
<Suspense fallback={<Skeleton />}>
<ClientData dataPromise={fetchData()} />
</Suspense>;
When reviewing React components, report:
## 🎯 useEffect Audit
**File**: `components/example.tsx`
**useEffect count**: 3 (Target: 0)
### 🚨 Must eliminate
- **Line 24**: `useEffect` fetching user data
- **Problem**: Data fetching in client component
- **Fix**: Move to parent Server Component, pass as prop
### ⚠️ Should eliminate
- **Line 45**: `useEffect` computing `fullName` from `firstName` + `lastName`
- **Problem**: Derived state stored in useState
- **Fix**: `const fullName = \`${firstName} ${lastName}\``
### ✅ Acceptable (with notes)
- **Line 67**: `useEffect` for D3 chart initialization
- **Note**: External library integration. Extract to `useD3Chart` hook.
| You're doing... | Don't use | Use instead |
|---|---|---|
| Data fetching | useEffect + fetch | Server Component |
| Form submission | useEffect + fetch POST | Server Action |
| Derived values | useEffect + setState | Compute inline |
| Event side effects | useEffect watching state | Event handler |
| Subscriptions | useEffect + subscribe | useSyncExternalStore |
| Optimistic UI | useEffect + temp state | useOptimistic |
| Delayed updates | useEffect + debounce | useDeferredValue |
| Pending states | useEffect + loading flag | useTransition |
| Focus/scroll | useEffect + ref | Ref callback |
| Initialize from storage | useEffect + setState | Lazy initializer |
useEffectEventuse(), useOptimistic, useActionState, useFormStatus.github/instructions/react.instructions.md