Install any skill in seconds. Free to start, no credit card required.
Get Started Free →React/TypeScript-specific code review overlay. Extends the universal code-reviewer skill with React version-aware rules. Trigger when reviewing React components, hooks, context, Suspense boundaries, Server Components, Client Components, React Router, TanStack Query, Zustand, Redux, React Hook Form, or any .tsx/.jsx file in a React project. Keywords: React, useState, useEffect, useCallback, useMemo, useRef, useContext, useReducer, useTransition, Suspense, memo, RSC, Server Component, Client Compo
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-15 | ✗→✓ | ▲ Improved | 146% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 127% | 0% |
| case-01 | ✓→✗ | ▼ Worse | 29% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 118% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 136% | 0% |
This skill extends code-reviewer (the universal skill). Always apply the universal skill's full checklist first, then apply the React-specific rules in this file on top.
Composition order:
code-reviewer (universal pillars: correctness, security, performance, DRY, tests, docs)Run these commands before touching any code. Version determines which rules apply.
bash# React and key dependency versions cat package.json | grep -E '"(react|react-dom|react-router|@tanstack|zustand|redux|react-hook-form|next|vite|typescript|react-compiler)"' | head -20 # Check if React Compiler is enabled cat babel.config.* next.config.* vite.config.* 2>/dev/null | grep -i "compiler\|babel-plugin-react-compiler" | head -5 # Check for legacy APIs still in use grep -r "ReactDOM\.render\|ReactDOM\.hydrate\|forwardRef\|createClass" src/ --include="*.tsx" --include="*.jsx" -l 2>/dev/null | head -5 # Check for Server vs Client Components (Next.js App Router) grep -r "\"use client\"\|\"use server\"" src/ app/ --include="*.tsx" -l 2>/dev/null | head -5
Report at the top of your review:
🔍 Environment: React vX.Y | TypeScript X.Y | Framework: Next.js X.Y / Vite / CRA
Compiler: Enabled / Disabled | RSC: Yes / No | Router: App Router / Pages / React Router vX
State: TanStack Query / Zustand / Redux / Context onlyThen apply the version-specific rules below.
import React from 'react' if new JSX transform is not configured.useTransition, useDeferredValue, Suspense for data fetching.ReactDOM.render() — upgrade path to createRoot() needed before moving to React 18+.useTransition to stay responsive.ReactDOM.flushSync() used unnecessarily; state updates are batched automatically in React 18 including in setTimeout, Promise, native events.Suspense for data — flag useEffect + useState for data fetching; prefer TanStack Query with Suspense or use() (React 19).StrictMode double-invocation — flag useEffect cleanup missing; Strict Mode intentionally double-fires effects in dev. Missing cleanup = real bug.createRoot required — flag ReactDOM.render() as Critical; it was removed in React 19.useDeferredValue — suggest for heavy filter/search lists where typing responsiveness matters.babel-plugin-react-compiler or Next.js reactCompiler), flag manual useMemo, useCallback, and React.memo as unnecessary noise — the compiler handles memoization automatically. Flag them as Low priority cleanup.forwardRef deprecated — flag React.forwardRef() in new code; ref is now a regular prop:tsx // ❌ Legacy forwardRef (deprecated in React 19) const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => ( <input ref={ref} {...props} /> ));
// ✅ ref as prop (React 19) function Input({ ref, ...props }: Props & { ref?: React.Ref<HTMLInputElement> }) { return <input ref={ref} {...props} />; }
ReactDOM.render() removed — flag as Critical. Must use createRoot().ReactDOM.hydrate() removed — flag as Critical. Must use hydrateRoot().isPending state + try/catch patterns for form submissions; use useActionState + useFormStatus:tsx // ❌ Manual pending/error state (React 18 pattern) const isPending, setIsPending] = useState(false); const handleSubmit = async () => { setIsPending(true); try { await submit(data); } finally { setIsPending(false); } };
// ✅ useActionState (React 19) const state, submitAction, isPending] = useActionState(submit, null);
use() hook — flag useEffect data fetching in components that could use use(promise) with Suspense.useOptimistic — flag optimistic UI patterns implemented manually with useState + rollback logic; suggest useOptimistic.string refs removed — flag ref="myRef" string refs (very old pattern) as Critical.import React from 'react' used only for JSX (not needed with new transform).key prop — flag any .map() rendering elements without a key.key={index} on mutable lists — flag key={index} on lists that can be reordered, filtered, or have items added/removed; use a stable unique ID:tsx // ❌ Index as key — causes state bugs on reorder/filter {users.map((user, i) => <UserCard key={i} user={user} />)}
// ✅ Stable ID {users.map(user => <UserCard key={user.id} user={user} />)}
key on fragments — flag missing key on <Fragment> in mapped lists.useEffect — flag useEffect + useState for API calls in React 18/19 projects. Recommend TanStack Query, use() + Suspense, or RSC:tsx // ❌ Manual fetch — no caching, no deduplication, race conditions useEffect(() => { fetch('/api/users').then(r => r.json()).then(setUsers); }, ]);
// ✅ TanStack Query — caching, deduplication, background refetch const { data: users } = useQuery({ queryKey: 'users'], queryFn: fetchUsers });
useEffect with subscriptions, timers, or event listeners missing a cleanup return function.useEffect — flag useEffect used to set state derived from props or other state; compute during render instead:tsx // ❌ Derived state in effect — extra render cycle useEffect(() => { setFullName(${firstName} ${lastName}); }, firstName, lastName]);
// ✅ Computed during render const fullName = ${firstName} ${lastName};
useEffect with incomplete dependency arrays. Every reactive value used inside must be in the array.useEffect referencing state/props that aren't in the dependency array.useEffect for event handlers — flag useEffect used to handle button clicks or user interactions; use event handlers directly.useMemo/useCallback on cheap operations adds overhead. Only memoize when: (a) computation is provably expensive (profiler evidence), or (b) referential equality matters for a child wrapped in React.memo.React.memo without stable props — flag React.memo on components receiving new object/array/function references every render — memoization is defeated.useState — flag state that is just a transformation of other state or props; compute during render.tsx // ❌ Overwrites entire state object setState({ name: 'Alice' });
// ✅ Preserve other fields setState(prev => ({ ...prev, name: 'Alice' }));
useReducer for complex state — suggest useReducer when: multiple useState values are always updated together, next state depends on previous state, or state has complex update logic.useState; use initializer function:tsx // ❌ Runs on every render const state, setState] = useState(expensiveComputation());
// ✅ Runs once const state, setState] = useState(() => expensiveComputation());
useState used for values that don't need to trigger re-renders (e.g., previous values, timers, DOM references); use useRef.ref.current accessed during render (before mount); DOM refs are only available after mount.Context used for high-frequency state (mouse position, scroll position); causes all consumers to re-render. Use Zustand or split contexts.&& — flag {count && <Component />} — renders 0 when count is 0. Use ternary:tsx // ❌ Renders "0" to DOM when count is 0 {count && <UserList />}
// ✅ Renders nothing {count > 0 && <UserList />} // or {count ? <UserList /> : null}
await calls for independent data; use Promise.all():tsx // ❌ Sequential — slow const user = await fetchUser(id); const posts = await fetchPosts(id);
// ✅ Parallel const user, posts] = await Promise.all(fetchUser(id), fetchPosts(id)]);
import { Button } from '../components' barrel files; import directly to avoid bundling unused exports.tsx // ❌ Eager import — increases initial bundle import HeavyChart from './HeavyChart';
// ✅ Lazy import — split into separate chunk const HeavyChart = lazy(() => import('./HeavyChart'));
tsx // ❌ New array reference on every render <List items={1, 2, 3]} />
// ✅ Stable reference const ITEMS = 1, 2, 3]; <List items={ITEMS} />
useDeferredValue for heavy lists — suggest for search/filter inputs rendering large lists; keeps input responsive.useTransition for non-urgent updates — flag navigation or tab-switching state updates that block input; mark as transitions.'use client' boundary placement — flag 'use client' placed too high in the tree, converting entire subtrees to client components unnecessarily. Push boundaries as deep as possible.useState, useEffect, useContext etc. in Server Components (no 'use client' directive) — these crash at runtime.useEffect + useState for data in RSC projects; fetch directly in Server Components:tsx // ✅ Server Component — fetch directly, no useEffect async function UserList() { const users = await fetchUsers(); // runs on server return users.map(u => <UserCard key={u.id} user={u} />); }
Date objects) from Server to Client Components as props — must be JSON-serializable.React.cache() — flag repeated identical fetches in Server Components without React.cache() for per-request deduplication.after() (Next.js 15+) — flag non-blocking post-response work (analytics, logging) blocking the response; use after().State management decision guide (flag misuse):
| State type | Right tool | |---|---| | Local component state | useState / useReducer | | Shared UI state (modals, theme) | Zustand or Context | | Server/API data | TanStack Query | | Form state | React Hook Form | | Global complex state with effects | Redux Toolkit or Zustand |
useEffect for server state — flag manual useEffect fetch + useState loading/error pattern; replace with TanStack Query.queryClient.invalidateQueries() or using onSuccess to update cache after a mutation that affects a list.react-hook-form register() with useState for the same field — pick one approach.resolver (Zod/Yup) schema validation; always validate before processing.useFormStatus — flag manual isSubmitting state when useFormStatus (React 19) is available.defaultValues — flag forms missing defaultValues in useForm() — causes uncontrolled-to-controlled warnings.dangerouslySetInnerHTML — flag any use without explicit sanitization (DOMPurify or equivalent). This is an XSS vector.href/src — flag dynamic href={userInput} without validation — allows javascript: injection.eval() / new Function() — flag any dynamic code execution.Flag these immediately when spotted:
| Anti-pattern | Severity | Fix | |---|---|---| | ReactDOM.render() | Critical | Use createRoot() | | ReactDOM.hydrate() | Critical | Use hydrateRoot() | | dangerouslySetInnerHTML without sanitization | Critical | Use DOMPurify | | key={index} on mutable lists | High | Use stable unique ID | | useEffect for data fetching | High | Use TanStack Query or RSC | | Missing useEffect cleanup | High | Add cleanup return function | | {count && <Comp />} pattern | High | Use ternary | | Derived state in useEffect | High | Compute during render | | Sequential await for independent data | High | Use Promise.all() | | Hooks in Server Components | Critical | Add 'use client' or remove hooks | | Non-serializable props from Server to Client | High | Serialize data before passing | | useMemo/useCallback with React Compiler | Low | Remove — compiler handles it | | forwardRef in React 19 | Low | Use ref as prop | | Barrel imports for large libs | Medium | Direct imports | | Lazy state init as expression useState(fn()) | Medium | Use useState(() => fn()) | | Redux storing API data | Medium | Use TanStack Query | | Large Context with frequent updates | Medium | Split context or use Zustand | | Missing key on mapped elements | High | Add stable key prop | | Stale closure in useEffect | High | Fix dependencies |
Use the same format as code-reviewer (universal). Add a React context line:
🔍 Environment: React v19.x | TypeScript 5.x | Next.js 15.x (App Router)
Compiler: Enabled | RSC: Yes | TanStack Query v5 | Zustand v5
## Code Review Summary
[... standard universal format ...]
### ⚛️ React-Specific Issues
[Issues found by this overlay, using the same severity/format as universal]useMemo as unnecessary is only valid when the React Compiler is confirmed enabled.useEffect is not for data fetching — in React 18/19 projects, this is always flagged. Exceptions: external subscriptions, DOM side effects, window event listeners.useMemo/useCallback are sometimes correct. Only flag when profiler evidence is absent AND the overhead isn't justified.security-auditor skill if available.Other measured skills in the registry, with their headline benchmark lift.