Web Development
ReactCode ReviewPerformanceAccessibilityTestingTypeScript

The React Code Review Checklist We Use: Performance, Accessibility, and Maintainability

AO
Adrijan Omićević
·14 min read

# Introduction#

A React PR can look clean and still ship regressions: a component rerenders on every keystroke, a modal traps focus incorrectly, or a hook silently creates stale state. The cost shows up later as slow pages, user complaints, and fragile code that nobody wants to touch.

This post is the React code review checklist we use at Samioda for production apps. It is structured for real PRs and includes examples for components, hooks, state, rendering, accessibility, and testing, plus a copy-paste template for GitHub or Linear and guidance on what to automate with lint rules.

For deeper dives, also keep these handy during reviews: our React accessibility checklist, our React performance profiling guide, and our web application security checklist.

# How to use this checklist in real PRs#

A checklist is only useful if it reduces risk without slowing shipping. Our approach is simple: automate what can be automated and reserve human review for things that require judgment.

Review workflow that scales#

  1. 1
    Run automation first: formatting, lint, typecheck, unit tests, and basic a11y linting should run in CI and locally.
  2. 2
    Do a quick diff scan: identify what changed and what could be affected. A small UI change can still impact rendering, focus management, or error handling.
  3. 3
    Review by risk areas: performance, a11y, maintainability, and tests. The sections below map directly to that.
  4. 4
    Request changes with a reason: reviewers should explain the user impact, not personal style preferences.

What to automate versus what to review manually#

CategoryAutomate with toolingReview manually
FormattingPrettierNone
Type safetyTypeScript strict mode, tsc --noEmitAPI design, domain modeling
Code qualityESLint, React Hooks lint, import rulesNaming, separation of concerns
Accessibilityeslint-plugin-jsx-a11y, Storybook a11yKeyboard flows, focus traps, complex widgets
PerformanceBuild warnings, bundle analyzerRerender triggers, memo boundaries, large lists
TestingJest, React Testing Library, coverage gatesTest quality, real user scenarios
SecurityDependency scanning, secret scanningAuthorization logic, data exposure

ℹ️ Note: Static tools catch a lot, but they cannot fully validate keyboard UX, actual rerender cost, or whether tests cover meaningful behavior. The goal is fewer debates about basics and more attention on product risk.

# Checklist Part 1: Components and API design#

Good React components are predictable, easy to reuse, and hard to misuse. Most long-term maintainability issues start with unclear component responsibilities and unstable APIs.

Component responsibilities and boundaries#

Check

  • The component does one job. If it fetches data, transforms it, and renders multiple unrelated sections, it is a refactor candidate.
  • Container logic and presentational UI are separated when complexity grows.
  • Shared logic is extracted into hooks or utilities, not duplicated.

Example: split data and UI when behavior grows

TSX
// Good: UI stays testable and reusable
type UserCardProps = {
  user: { id: string; name: string; email: string };
  onEdit: (id: string) => void;
};
 
export function UserCard({ user, onEdit }: UserCardProps) {
  return (
    <section aria-label="User">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      <button type="button" onClick={() => onEdit(user.id)}>
        Edit
      </button>
    </section>
  );
}

Props, defaults, and typing#

Check

  • Prop types are explicit and minimal. Avoid “bag of props” patterns.
  • Optional props have clear defaults, ideally at the parameter level.
  • Callback prop names represent intent, not implementation details.

Common smell

  • A component takes data: any and setData: any and acts as a mini-app.

Better

  • Pass only the data it needs and intent-based callbacks like onSubmit or onSelect.

Errors, loading, and empty states#

Check

  • Error and loading states are handled at the right level.
  • Empty states are intentional and accessible.
  • UI does not “flash” between states unnecessarily.

Concrete review question

  • If the API returns an empty array, what does the user see and can they recover?

Composition and variants#

Check

  • Variants use composition rather than conditional chaos.
  • If the component has more than 3 to 4 variant props, consider splitting into subcomponents.

Example: avoid prop explosion

  • Instead of variant="primary" size="sm" iconLeft="..." iconRight="..." loading, consider Button plus Button.Icon plus Button.Loading patterns where appropriate.

# Checklist Part 2: Hooks and side effects#

Hooks bugs are expensive because they often pass manual QA and fail only under timing or navigation edge cases.

React Hooks correctness#

Check

  • useEffect dependencies are correct, with no missing dependencies “because it works”.
  • Effects are used for synchronization, not for deriving UI state.
  • Cleanup functions exist for subscriptions, timers, and event listeners.

Bad: effect runs too often and triggers extra state

TSX
useEffect(() => {
  setFormValue(user.name);
}, [user]);

Better: depend on the actual value

TSX
useEffect(() => {
  setFormValue(user.name);
}, [user.name]);

Stable callbacks and derived values#

Check

  • useCallback and useMemo are used when they prevent measurable rerenders or expensive work, not as a default.
  • Derived data is computed from source state, not stored as additional state.

Smell

  • const [filtered, setFiltered] = useState(...) where filtered is always derived from items and query.

Better

  • Compute filtered with memoization only if it is expensive.
TSX
const filtered = useMemo(() => {
  return items.filter((i) => i.name.toLowerCase().includes(query.toLowerCase()));
}, [items, query]);

Custom hooks API#

Check

  • Custom hooks expose a small, coherent return shape.
  • Async hooks handle cancellation or ignore stale responses.
  • Error state is explicit.

Example: ignore stale responses

TSX
useEffect(() => {
  let active = true;
 
  (async () => {
    const result = await fetchUser(userId);
    if (active) setUser(result);
  })();
 
  return () => {
    active = false;
  };
}, [userId]);

⚠️ Warning: Using useEffect for async work without guarding against stale responses can display the wrong data after fast navigation or rapid input changes.

# Checklist Part 3: State management and data flow#

Most React complexity is self-inflicted via unclear state ownership. A review should clarify what is local state, what is server state, and what is derived.

State ownership and colocating state#

Check

  • State lives where it is used. Lift state up only when necessary.
  • Avoid “global state by default”. Global state makes dependencies implicit.
  • Use server-state tooling for server data patterns when applicable.

Concrete review question

  • Can this state be derived from props or URL params instead of stored?

Avoiding duplicated and inconsistent state#

Check

  • No duplicated sources of truth like isOpen and selectedId that can drift.
  • Complex forms use a consistent approach, not ad-hoc state per field.

Example smell

  • Both items and itemsCount stored separately.

Reducers for multi-step interactions#

Check

  • When state transitions are non-trivial, useReducer or a state machine is considered.
  • Action naming reflects user intent.

Example: reducer clarifies transitions

TSX
type State = { status: "idle" | "saving" | "error" };
type Action = { type: "save" } | { type: "success" } | { type: "fail" };
 
function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "save":
      return { status: "saving" };
    case "success":
      return { status: "idle" };
    case "fail":
      return { status: "error" };
    default:
      return state;
  }
}

# Checklist Part 4: Rendering and performance#

Performance issues usually come from unnecessary rerenders, expensive work inside render, and unbounded lists. Modern React can be fast, but it is also easy to accidentally defeat optimizations.

For a more detailed workflow with React DevTools Profiler and memoization patterns, use our guide on React performance profiling and rendering patterns.

Preventing unnecessary rerenders#

Check

  • Props passed to memoized children are stable when needed.
  • Inline object and array literals are not passed to deep trees without a reason.
  • Event handlers are stable when they matter.

Bad: unstable prop causes child rerenders

TSX
<Chart options={{ showLegend: true }} data={data} />

Better: memoize stable objects

TSX
const chartOptions = useMemo(() => ({ showLegend: true }), []);
<Chart options={chartOptions} data={data} />

List rendering and keys#

Check

  • Lists use stable keys from data, not array indexes.
  • Very large lists use virtualization.
  • Rendering work scales with data size.

Concrete benchmark

  • Once you approach hundreds of DOM rows, scrolling and rerenders become visibly worse on mid-range devices. Virtualization is often worth it at 500 to 1,000 items depending on row complexity.

Conditional rendering and layout shifts#

Check

  • UI avoids layout jumps where possible. Reserve space for loaders and images.
  • Skeletons or placeholders do not break keyboard navigation.

Avoid expensive computations in render#

Check

  • Sorting, grouping, and formatting is moved out of render or memoized.
  • Date and number formatting in large tables is cached or precomputed.

💡 Tip: If a component renders more than once per user interaction, profile before adding memoization. In many apps, the biggest win comes from removing redundant state and fixing effect dependencies, not from wrapping everything in memo.

# Checklist Part 5: Accessibility and UX basics#

Accessibility is not a “later” task. Reviews are the best place to catch issues before they become patterns repeated across the codebase.

Use our detailed React accessibility a11y checklist for deeper ARIA and focus guidance.

Semantic HTML first#

Check

  • Use semantic elements for intent: button for actions, a for navigation, headings in order.
  • Avoid clickable div or span. If it must exist, it needs keyboard handlers and role, but that is still second-best.

Example: correct button usage

TSX
<button type="button" onClick={onDelete} aria-label="Delete user">
  Delete
</button>

Accessible names and labels#

Check

  • Inputs have labels linked via htmlFor and id.
  • Icon-only buttons have aria-label.
  • aria-describedby is used for hint and error text.

Keyboard navigation and focus management#

Check

  • Modals trap focus and return focus on close.
  • Dropdowns and menus work with keyboard, including Escape to close.
  • Focus is visible and not removed.

Concrete review question

  • Can you complete the full flow using Tab, Shift+Tab, Enter, Space, and Escape only?

ARIA usage#

Check

  • ARIA is used only when semantic HTML cannot express the behavior.
  • No conflicting attributes like aria-hidden on focusable elements.

Motion and reduced motion#

Check

  • Animations respect user preferences where relevant.
  • Transition-heavy UI does not block interactions.

# Checklist Part 6: Testing and QA signals#

Tests should protect behavior users care about, not implementation details. In reviews, we look for tests that fail when UX breaks.

Unit and component tests#

Check

  • Tests use user-centric queries and interactions.
  • Avoid testing internal state or private functions.
  • Cover error states and empty states for critical flows.

Example: React Testing Library style

TSX
it("submits the form", async () => {
  render(<UserForm />);
  await user.type(screen.getByLabelText("Email"), "a@b.com");
  await user.click(screen.getByRole("button", { name: "Save" }));
  expect(await screen.findByText("Saved")).toBeInTheDocument();
});

What to test, by risk#

AreaMinimum expectationWhy it matters
FormsValidation, submit success, submit errorPrevents silent data loss
NavigationRoute changes and back behaviorAvoids broken flows
PermissionsUI states for unauthorized usersPrevents data exposure
AccessibilityRole and name checks, focus basicsStops regressions early
Performance-sensitive viewsSmoke test for heavy listsPrevents slowdowns

E2E tests for user journeys#

Check

  • Add E2E tests for revenue-critical flows and complex interactions.
  • Keep E2E focused. Too many brittle tests slow delivery.

# Reusable PR checklist template for GitHub and Linear#

Copy this into your PR description template or Linear issue. Keep the required items short and force authors to think about impact, not just implementation.

Markdown
## Summary
- What changed:
- Why:
- User impact:
- Screenshots or video:
 
## React Code Review Checklist
### Components
- [ ] Component has a single clear responsibility
- [ ] Props are minimal, well-typed, and intent-based
- [ ] Loading, empty, and error states handled
 
### Hooks and effects
- [ ] `useEffect` dependencies correct and no stale closures
- [ ] Cleanup added for subscriptions, timers, and listeners
- [ ] Derived values are not duplicated in state
 
### State and data flow
- [ ] State ownership is clear and colocated where used
- [ ] No duplicated sources of truth
- [ ] Reducer or state machine considered for complex transitions
 
### Rendering and performance
- [ ] Lists use stable keys and avoid index keys
- [ ] No expensive computations inside render without memoization
- [ ] Memoization used only when it prevents real rerenders
 
### Accessibility
- [ ] Semantic elements used for interactive UI
- [ ] All inputs have labels and errors are announced
- [ ] Keyboard navigation works for the full flow
- [ ] Focus management handled for modals and overlays
 
### Testing
- [ ] Unit or component tests cover key behaviors and edge cases
- [ ] Tests query by role/label/text instead of implementation details
- [ ] E2E test added or updated for high-risk user journeys
 
## Risk notes
- Potential regressions:
- Rollback plan:

# What to automate with lint rules and CI checks#

If a reviewer repeatedly comments on the same issue, it should become a rule. The goal is fewer human cycles spent on preventable mistakes.

GoalToolingWhat it prevents
Consistent formattingPrettier in CI and pre-commitStyle debates and noisy diffs
Hook correctnesseslint-plugin-react-hooksMissing dependencies and stale closures
A11y basicseslint-plugin-jsx-a11yMissing labels, wrong roles, keyboard traps
Import hygieneeslint-plugin-import, no cycle rulesCircular deps and messy module boundaries
Type safetyTypeScript strict and noImplicitAnyRuntime errors and unclear contracts
Test gateUnit tests in CI, optional coverage thresholdShipping without safety net

ESLint configuration sketch#

Keep it short and pragmatic. The exact config depends on your stack, but these are common essentials.

JavaScript
module.exports = {
  extends: [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:jsx-a11y/recommended",
  ],
  rules: {
    "react/jsx-key": "error",
    "react/self-closing-comp": "warn",
    "jsx-a11y/label-has-associated-control": "error",
    "jsx-a11y/no-noninteractive-element-interactions": "error",
    "jsx-a11y/click-events-have-key-events": "error",
  },
};

CI checks we consider non-negotiable#

  1. 1
    npm run lint
  2. 2
    npm run typecheck
  3. 3
    npm test
  4. 4
    Build step for the app, especially for Next.js routes

Add security scanning and dependency checks as your codebase grows, aligned with our web application security checklist.

🎯 Key Takeaway: Anything that can be reliably detected by static analysis should be automated, so reviewers can spend time on architecture, UX, performance risks, and accessibility flows.

# Key Takeaways#

  • Automate repeatable review items with ESLint, TypeScript, and CI, then focus human review on UX, a11y, and performance risks.
  • Review components for clear responsibilities, minimal props, and intentional loading, empty, and error states.
  • Verify hooks and effects for correct dependencies, cleanup, and avoiding duplicated derived state.
  • Catch performance regressions early by checking stable props, list keys, expensive render work, and virtualization for large lists.
  • Treat accessibility as a first-class review area: semantic HTML, accessible names, keyboard flows, and correct focus management.
  • Require tests that reflect user behavior and cover critical edge cases, not internal implementation details.

# Conclusion#

A reliable React code review checklist prevents the same problems from shipping repeatedly: slow rendering from unstable props, inaccessible UI patterns, and fragile state logic. If you standardize the checklist, automate the basics, and train reviewers to focus on risk, your PRs get faster and your app gets more stable.

If you want help implementing this checklist across a React or Next.js codebase, or you need a review of your current performance and accessibility bottlenecks, reach out to Samioda via our web development services and we will tailor a review process that fits your team and release cadence.

FAQ

Share
A
Adrijan OmićevićFounder & Senior Developer

Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.