Code Reviewer — React Overlay
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:
- Run
code-reviewer(universal pillars: correctness, security, performance, DRY, tests, docs) - Run this overlay (React/TypeScript-specific rules below)
- Report findings from both in a single unified output
Step 0 — Detect Versions First (always, before reviewing anything)
Run these commands before touching any code. Version determines which rules apply.
# 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 only
Then apply the version-specific rules below.
React Version Rules
React 17
- Legacy JSX transform — flag missing
import React from 'react'if new JSX transform is not configured. - No concurrent features — don't suggest
useTransition,useDeferredValue,Suspensefor data fetching. - Flag
ReactDOM.render()— upgrade path tocreateRoot()needed before moving to React 18+. - ⚠️ EOL — flag as Warning; no active maintenance. Suggest upgrade to React 18 minimum.
React 18
- Concurrent rendering available — flag expensive synchronous renders that could use
useTransitionto stay responsive. - Automatic batching — flag manual
ReactDOM.flushSync()used unnecessarily; state updates are batched automatically in React 18 including insetTimeout,Promise, native events. Suspensefor data — flaguseEffect+useStatefor data fetching; prefer TanStack Query with Suspense oruse()(React 19).StrictModedouble-invocation — flaguseEffectcleanup missing; Strict Mode intentionally double-fires effects in dev. Missing cleanup = real bug.createRootrequired — flagReactDOM.render()as Critical; it was removed in React 19.useDeferredValue— suggest for heavy filter/search lists where typing responsiveness matters.
React 19 ✅ Current
- React Compiler — if the compiler is enabled (
babel-plugin-react-compileror Next.jsreactCompiler), flag manualuseMemo,useCallback, andReact.memoas unnecessary noise — the compiler handles memoization automatically. Flag them as Low priority cleanup. forwardRefdeprecated — flagReact.forwardRef()in new code;refis now a regular prop:// ❌ 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 usecreateRoot().ReactDOM.hydrate()removed — flag as Critical. Must usehydrateRoot().- Actions API — flag manual
isPendingstate +try/catchpatterns for form submissions; useuseActionState+useFormStatus:// ❌ 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 — flaguseEffectdata fetching in components that could useuse(promise)with Suspense.useOptimistic— flag optimistic UI patterns implemented manually withuseState+ rollback logic; suggestuseOptimistic.stringrefs removed — flagref="myRef"string refs (very old pattern) as Critical.- New JSX transform required — flag
import React from 'react'used only for JSX (not needed with new transform).
React-Specific Review Checklist
🔑 Keys & Lists
- Missing
keyprop — flag any.map()rendering elements without akey. key={index}on mutable lists — flagkey={index}on lists that can be reordered, filtered, or have items added/removed; use a stable unique ID:// ❌ 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} />)}keyon fragments — flag missingkeyon<Fragment>in mapped lists.
🪝 Hooks
useEffect
- Data fetching in
useEffect— flaguseEffect+useStatefor API calls in React 18/19 projects. Recommend TanStack Query,use()+ Suspense, or RSC:// ❌ 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 }); - Missing cleanup — flag
useEffectwith subscriptions, timers, or event listeners missing a cleanupreturnfunction. - Derived state in
useEffect— flaguseEffectused to set state derived from props or other state; compute during render instead:// ❌ Derived state in effect — extra render cycle useEffect(() => { setFullName(`${firstName} ${lastName}`); }, [firstName, lastName]); // ✅ Computed during render const fullName = `${firstName} ${lastName}`; - Missing dependencies — flag
useEffectwith incomplete dependency arrays. Every reactive value used inside must be in the array. - Stale closure — flag callbacks inside
useEffectreferencing state/props that aren't in the dependency array. useEffectfor event handlers — flaguseEffectused to handle button clicks or user interactions; use event handlers directly.
useMemo / useCallback / React.memo
- With React Compiler enabled — flag all three as unnecessary; the compiler handles this automatically. Remove them as Low priority cleanup.
- Without React Compiler — flag over-use:
useMemo/useCallbackon cheap operations adds overhead. Only memoize when: (a) computation is provably expensive (profiler evidence), or (b) referential equality matters for a child wrapped inReact.memo. React.memowithout stable props — flagReact.memoon components receiving new object/array/function references every render — memoization is defeated.
useState / useReducer
- Derived state in
useState— flag state that is just a transformation of other state or props; compute during render. - Object spread for state updates — flag state updates missing spread of previous state:
// ❌ Overwrites entire state object setState({ name: 'Alice' }); // ✅ Preserve other fields setState(prev => ({ ...prev, name: 'Alice' })); useReducerfor complex state — suggestuseReducerwhen: multipleuseStatevalues are always updated together, next state depends on previous state, or state has complex update logic.- Lazy state initialization — flag expensive initial state computation passed directly to
useState; use initializer function:// ❌ Runs on every render const [state, setState] = useState(expensiveComputation()); // ✅ Runs once const [state, setState] = useState(() => expensiveComputation());
useRef
- Mutable values without re-render — flag
useStateused for values that don't need to trigger re-renders (e.g., previous values, timers, DOM references); useuseRef. - DOM access timing — flag
ref.currentaccessed during render (before mount); DOM refs are only available after mount.
useContext
- Context for frequently changing values — flag
Contextused for high-frequency state (mouse position, scroll position); causes all consumers to re-render. Use Zustand or split contexts. - Prop drilling vs Context — flag prop drilling more than 2-3 levels deep as a suggestion to evaluate Context or a state manager.
⚡ Performance
- Conditional rendering with
&&— flag{count && <Component />}— renders0when count is 0. Use ternary:// ❌ Renders "0" to DOM when count is 0 {count && <UserList />} // ✅ Renders nothing {count > 0 && <UserList />} // or {count ? <UserList /> : null} - Waterfall fetches — flag sequential
awaitcalls for independent data; usePromise.all():// ❌ Sequential — slow const user = await fetchUser(id); const posts = await fetchPosts(id); // ✅ Parallel const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]); - Barrel imports — flag
import { Button } from '../components'barrel files; import directly to avoid bundling unused exports. - Code splitting — flag large components imported statically that could be lazy-loaded:
// ❌ Eager import — increases initial bundle import HeavyChart from './HeavyChart'; // ✅ Lazy import — split into separate chunk const HeavyChart = lazy(() => import('./HeavyChart')); - Inline object/array props — flag object or array literals created inline in JSX passed to memoized children — creates new reference every render:
// ❌ New array reference on every render <List items={[1, 2, 3]} /> // ✅ Stable reference const ITEMS = [1, 2, 3]; <List items={ITEMS} /> useDeferredValuefor heavy lists — suggest for search/filter inputs rendering large lists; keeps input responsive.useTransitionfor non-urgent updates — flag navigation or tab-switching state updates that block input; mark as transitions.
🌐 Server Components / RSC (if Next.js App Router or RSC framework)
'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.- Hooks in Server Components — flag
useState,useEffect,useContextetc. in Server Components (no'use client'directive) — these crash at runtime. - Data fetching in Server Components — flag
useEffect+useStatefor data in RSC projects; fetch directly in Server Components:// ✅ 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} />); } - Serialization — flag passing non-serializable values (functions, class instances,
Dateobjects) from Server to Client Components as props — must be JSON-serializable. React.cache()— flag repeated identical fetches in Server Components withoutReact.cache()for per-request deduplication.after()(Next.js 15+) — flag non-blocking post-response work (analytics, logging) blocking the response; useafter().
🔄 State Management
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 |
- Redux for server state — flag Redux storing API data that TanStack Query would handle better (loading, caching, refetch, invalidation). Note: Zustand is often used alongside TanStack Query for client-side UI state — only flag Zustand when it duplicates server-state caching.
useEffectfor server state — flag manualuseEffectfetch +useStateloading/error pattern; replace with TanStack Query.- TanStack Query mutation cache — flag mutations not calling
queryClient.invalidateQueries()or usingonSuccessto update cache after a mutation that affects a list. - Zustand slice isolation — flag a single large Zustand store for unrelated state; split into feature slices.
- Context re-render problem — flag a single large Context value object where part of the state changes frequently; split into separate contexts or use Zustand.
📝 Forms (React Hook Form)
- Uncontrolled vs controlled — flag mixing
react-hook-formregister()withuseStatefor the same field — pick one approach. - Validation — flag form submissions without
resolver(Zod/Yup) schema validation; always validate before processing. useFormStatus— flag manualisSubmittingstate whenuseFormStatus(React 19) is available.defaultValues— flag forms missingdefaultValuesinuseForm()— causes uncontrolled-to-controlled warnings.
🔐 Security (React-specific)
dangerouslySetInnerHTML— flag any use without explicit sanitization (DOMPurifyor equivalent). This is an XSS vector.- User-controlled URLs in
href/src— flag dynamichref={userInput}without validation — allowsjavascript:injection. - Sensitive data in state — flag passwords, tokens, or PII stored in component state that gets serialized to RSC payloads or localStorage.
eval()/new Function()— flag any dynamic code execution.
Anti-Patterns Quick Reference
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 |
Unified Output Format
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]
Behavior Rules (React-specific additions)
- Version first, always — never apply compiler-specific or React 19 rules without confirming the version. Flagging
useMemoas unnecessary is only valid when the React Compiler is confirmed enabled. useEffectis not for data fetching — in React 18/19 projects, this is always flagged. Exceptions: external subscriptions, DOM side effects,windowevent listeners.- Don't over-flag memoization — without the compiler,
useMemo/useCallbackare sometimes correct. Only flag when profiler evidence is absent AND the overhead isn't justified. - RSC boundary decisions are architectural — flag incorrect hook usage in Server Components as Critical, but don't opine on where to place the boundary without context.
- Delegate deep security audits →
security-auditorskill if available. - React Native is out of scope — this skill covers React for web. React Native has different rules (no DOM, different performance model).