Skip to content

Instantly share code, notes, and snippets.

@Kasun002
Last active May 14, 2026 03:14
Show Gist options
  • Select an option

  • Save Kasun002/deeadb834fbcceb998c2e0ba7db70e69 to your computer and use it in GitHub Desktop.

Select an option

Save Kasun002/deeadb834fbcceb998c2e0ba7db70e69 to your computer and use it in GitHub Desktop.
Senior React Code Review Checklist

Senior React Code Review Checklist

A pragmatic, battle-tested checklist for reviewing React codebases (React 19+, Hooks era, Next.js-aware). Use it as a scan-during-PR reference — not a rulebook. Context always wins.


1. Project Setup

  • README.md present: Setup, scripts, env vars, architecture diagram. A new dev should be productive in <30 min.
  • .gitignore: Excludes node_modules/, .env*, dist/, build/, .DS_Store, coverage/, IDE folders.
  • package.json hygiene: Pinned or caret-versioned deps, no unused packages, engines field set, scripts documented.
  • Lockfile committed: package-lock.json / pnpm-lock.yaml / yarn.lock — never both.
  • ESLint + Prettier configured: Shared config (e.g., eslint-config-airbnb, @typescript-eslint), react-hooks/exhaustive-deps rule on, Prettier runs on pre-commit (Husky + lint-staged).
  • TypeScript: strict: true in tsconfig.json — non-negotiable for new projects.
  • Node version pinned: .nvmrc or volta config to avoid "works on my machine".
Aspect ✅ Good ❌ Bad
Dependencies Lean, audited, no duplicates 200+ deps, unused libs, security warnings
Scripts dev, build, test, lint, typecheck Only start

2. Code Style & Readability

  • Naming: camelCase for variables/functions, PascalCase for components, UPPER_SNAKE for constants, useXxx for hooks. Boolean prefixes: is, has, should.
  • No var, no console.log left in committed code. Use const by default, let when reassigning. Strip logs via ESLint no-console.
  • Consistent imports: Absolute paths (@/components/Button) over ../../../. Group: external → internal → relative → styles.
  • Destructuring: Props, state, and objects destructured at the top of the function for clarity.
  • Comments explain why, not what: Code shows what; comments justify decisions, trade-offs, or workarounds.
  • Magic numbers/strings: Extract to named constants.
// ❌ Bad
if (user.role === 2) { ... }

// ✅ Good
const ROLE_ADMIN = 2;
if (user.role === ROLE_ADMIN) { ... }

3. Components

  • Single Responsibility: One component = one job. If you describe it with "and", split it.
  • Size discipline: Aim <200 lines. Beyond that, extract subcomponents, hooks, or utilities.
  • Functional components only: Class components are legacy. New code should never introduce them.
  • No prop mutation: Props are read-only. Treat them as immutable.
  • Typed props: Use TypeScript interfaces (preferred) or PropTypes + defaultProps. Never leave props untyped in shared components.
  • Composition over configuration: Prefer children and slot patterns over giant prop APIs.
  • Co-locate: Component + styles + tests + types in one folder (Button/index.tsx, Button.test.tsx, Button.module.css).
// ✅ Good
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
}
export const Button = ({ label, onClick, variant = 'primary' }: ButtonProps) => ( ... );

4. Hooks & State

  • Rules of Hooks: Call only at the top level, only in components or custom hooks. No conditionals, loops, or nested functions.
  • Exhaustive deps: Every reactive value used inside useEffect/useMemo/useCallback must be in the dependency array. Don't suppress the lint rule — fix the design.
  • Custom hooks for reuse: Any logic used in 2+ components belongs in useSomething. Prefix with use.
  • Minimal state: Derive whenever possible — don't store what you can compute.
  • Lift state only as far as needed: Don't push state to the root "just in case".
  • React 19 awareness: Use useActionState, useOptimistic, useFormStatus, and the new use() hook where appropriate. The compiler (React Compiler) may make manual memoization redundant — review accordingly.
// ❌ Bad: storing derived state
const [fullName, setFullName] = useState('');
useEffect(() => setFullName(`${first} ${last}`), [first, last]);

// ✅ Good: derive it
const fullName = `${first} ${last}`;

5. JSX & Rendering

  • Semantic HTML: <button>, <nav>, <main>, <article> — not <div onClick> everywhere.
  • No heavy logic in JSX: Extract complex expressions to variables or helper functions above return.
  • Stable, unique keys: Use entity IDs. Never use array index unless the list is static and never reordered.
  • Safe conditional rendering: Beware && with numbers — 0 && <X /> renders 0. Use ternaries or explicit booleans.
  • Fragments over wrapper divs: Use <>...</> to avoid DOM bloat.
  • Avoid inline object/function props in hot paths — they break memoization.
// ❌ Bad
{items.map((item, i) => <Row key={i} {...item} />)}
{count && <Badge />}

// ✅ Good
{items.map(item => <Row key={item.id} {...item} />)}
{count > 0 && <Badge />}

6. Performance

  • useMemo / useCallback / React.memo: Apply where profiling shows benefit — not by default. With React Compiler, manual memoization is often unnecessary.
  • Avoid unnecessary re-renders: Stable references, split contexts, colocate state.
  • Code splitting: React.lazy + <Suspense> for routes and heavy components.
  • Virtualize long lists: Use react-window or @tanstack/react-virtual for lists >100 items.
  • Image optimization: Lazy load, use next/image (Next.js) or loading="lazy", serve modern formats (WebP/AVIF).
  • Bundle audit: Run vite-bundle-visualizer or @next/bundle-analyzer. Flag deps >50KB.
  • Profile before optimizing: React DevTools Profiler is the source of truth.
Technique When to Use
React.memo Pure component re-rendering with same props
useMemo Expensive computation, stable reference needed
useCallback Function passed to memoized child or in deps array
Virtualization Lists with >100 items or large DOM trees

7. State Management

  • Local first: useState/useReducer for component-local concerns.
  • Lift carefully: Share via props 1-2 levels; beyond that, reach for context or a store.
  • Global state library: Pick oneZustand (simple), Redux Toolkit (large teams, devtools), Jotai (atomic). Don't mix.
  • Server state ≠ client state: Use TanStack Query or SWR for server data — never stuff API responses into Redux manually.
  • No prop drilling >2 levels: Use context or composition.
  • Immutable updates: Spread, structuredClone, or Immer. Never mutate state directly.
// ❌ Bad
state.users.push(newUser);

// ✅ Good
setState(prev => ({ ...prev, users: [...prev.users, newUser] }));

8. Error Handling

  • Error Boundaries: Wrap routes and risky subtrees. Use react-error-boundary for ergonomic API.
  • Async try/catch: Every await in event handlers and effects has error handling.
  • User-friendly messages: Never show raw stack traces or [object Object]. Map errors to actionable UI.
  • Log to monitoring: Sentry, Datadog, or similar — don't just console.error.
  • Loading + error + empty + success: All four states handled in data-fetching components.
// ✅ Good
<ErrorBoundary fallback={<ErrorPage />}>
  <Suspense fallback={<Spinner />}>
    <Dashboard />
  </Suspense>
</ErrorBoundary>

9. Testing

  • Unit tests: Components, hooks, utilities. Use Vitest or Jest + React Testing Library.
  • Integration tests: User flows across multiple components — these catch the bugs unit tests miss.
  • E2E for critical paths: Playwright or Cypress for login, checkout, etc.
  • Coverage target: ~80% as a signal, not a goal. 100% coverage of garbage is still garbage.
  • Test behavior, not implementation: Query by role/label/text, not class names or component internals.
  • Mock at the boundary: Mock fetch/network (MSW preferred), not your own modules.
✅ Good Test ❌ Bad Test
getByRole('button', { name: /submit/i }) container.querySelector('.btn-primary')
Asserts user-visible behavior Asserts internal state shape

10. Accessibility (a11y)

  • Semantic elements first: A <button> already has focus, keyboard, and ARIA built in. Use it.
  • ARIA only when needed: ARIA is a patch, not a replacement for semantic HTML.
  • Keyboard navigation: Every interactive element reachable via Tab, operable via Enter/Space. Visible focus rings.
  • Alt text: Meaningful for content images; alt="" for decorative ones.
  • Labels for inputs: <label htmlFor> or aria-label. Never rely on placeholder alone.
  • Color contrast: WCAG AA minimum (4.5:1 for text).
  • Tools: eslint-plugin-jsx-a11y, axe DevTools, screen reader spot-checks (VoiceOver/NVDA).

11. Security

  • No dangerouslySetInnerHTML unless content is sanitized (e.g., DOMPurify).
  • Sanitize user input: Both client and server. Trust nothing from the browser.
  • No secrets in code: API keys, tokens — use env vars and a secrets manager. Audit git history.
  • HTTPS everywhere: Including dev where feasible.
  • Auth tokens: HttpOnly cookies preferred over localStorage (XSS-exposed).
  • CSP headers: Mitigate XSS at the platform level.
  • Dependency audits: npm audit, Snyk, or Dependabot on a schedule.
// ❌ Bad
<div dangerouslySetInnerHTML={{ __html: userBio }} />

// ✅ Good
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userBio) }} />

12. Build, Deploy & Responsiveness

  • Zero warnings/errors in build output. Warnings become bugs.
  • Env vars: .env.local for dev, never committed. Validated at startup (e.g., zod).
  • Bundle analysis: Run before merging large features. Watch for accidental large imports (e.g., entire lodash).
  • Mobile responsive: Test at 320px, 768px, 1024px+. Mobile-first CSS.
  • Lighthouse / Core Web Vitals: LCP <2.5s, INP <200ms, CLS <0.1.
  • Next.js specifics: Correct use of Server vs Client Components ('use client' only where needed), next/image, next/font, route segment config, streaming with Suspense.
  • CI/CD: Lint, typecheck, test, build all pass before merge.

13. Best Practices & Final Polish

  • DRY: Extract repeated logic to utils/hooks — but don't over-abstract. Rule of three: duplicate twice, abstract on the third.
  • TypeScript: Strict mode, no any (use unknown + narrowing), shared types in a types/ folder.
  • File structure: Feature-based (features/auth/) scales better than type-based (components/, hooks/) for large apps.
  • Consistent formatting: Prettier + ESLint must pass in CI.
  • Accessibility, performance, and tests are not "later" — they're part of "done".
  • Commits & PRs: Conventional commits, small PRs (<400 lines), clear descriptions, screenshots for UI changes.
  • Document the gotchas: If something is non-obvious, a comment or ADR saves the next developer.

Usage: Print/Pin this checklist for every PR review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment