# 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#
- 1Run automation first: formatting, lint, typecheck, unit tests, and basic a11y linting should run in CI and locally.
- 2Do 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.
- 3Review by risk areas: performance, a11y, maintainability, and tests. The sections below map directly to that.
- 4Request changes with a reason: reviewers should explain the user impact, not personal style preferences.
What to automate versus what to review manually#
| Category | Automate with tooling | Review manually |
|---|---|---|
| Formatting | Prettier | None |
| Type safety | TypeScript strict mode, tsc --noEmit | API design, domain modeling |
| Code quality | ESLint, React Hooks lint, import rules | Naming, separation of concerns |
| Accessibility | eslint-plugin-jsx-a11y, Storybook a11y | Keyboard flows, focus traps, complex widgets |
| Performance | Build warnings, bundle analyzer | Rerender triggers, memo boundaries, large lists |
| Testing | Jest, React Testing Library, coverage gates | Test quality, real user scenarios |
| Security | Dependency scanning, secret scanning | Authorization 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
// 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: anyandsetData: anyand acts as a mini-app.
Better
- Pass only the data it needs and intent-based callbacks like
onSubmitoronSelect.
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, considerButtonplusButton.IconplusButton.Loadingpatterns 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
useEffectdependencies 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
useEffect(() => {
setFormValue(user.name);
}, [user]);Better: depend on the actual value
useEffect(() => {
setFormValue(user.name);
}, [user.name]);Stable callbacks and derived values#
Check
useCallbackanduseMemoare 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(...)wherefilteredis always derived fromitemsandquery.
Better
- Compute
filteredwith memoization only if it is expensive.
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
useEffect(() => {
let active = true;
(async () => {
const result = await fetchUser(userId);
if (active) setUser(result);
})();
return () => {
active = false;
};
}, [userId]);⚠️ Warning: Using
useEffectfor 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
isOpenandselectedIdthat can drift. - Complex forms use a consistent approach, not ad-hoc state per field.
Example smell
- Both
itemsanditemsCountstored separately.
Reducers for multi-step interactions#
Check
- When state transitions are non-trivial,
useReduceror a state machine is considered. - Action naming reflects user intent.
Example: reducer clarifies transitions
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
<Chart options={{ showLegend: true }} data={data} />Better: memoize stable objects
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:
buttonfor actions,afor navigation, headings in order. - Avoid clickable
divorspan. If it must exist, it needs keyboard handlers and role, but that is still second-best.
Example: correct button usage
<button type="button" onClick={onDelete} aria-label="Delete user">
Delete
</button>Accessible names and labels#
Check
- Inputs have labels linked via
htmlForandid. - Icon-only buttons have
aria-label. aria-describedbyis 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-hiddenon 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
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#
| Area | Minimum expectation | Why it matters |
|---|---|---|
| Forms | Validation, submit success, submit error | Prevents silent data loss |
| Navigation | Route changes and back behavior | Avoids broken flows |
| Permissions | UI states for unauthorized users | Prevents data exposure |
| Accessibility | Role and name checks, focus basics | Stops regressions early |
| Performance-sensitive views | Smoke test for heavy lists | Prevents 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.
## 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.
Recommended automation baseline#
| Goal | Tooling | What it prevents |
|---|---|---|
| Consistent formatting | Prettier in CI and pre-commit | Style debates and noisy diffs |
| Hook correctness | eslint-plugin-react-hooks | Missing dependencies and stale closures |
| A11y basics | eslint-plugin-jsx-a11y | Missing labels, wrong roles, keyboard traps |
| Import hygiene | eslint-plugin-import, no cycle rules | Circular deps and messy module boundaries |
| Type safety | TypeScript strict and noImplicitAny | Runtime errors and unclear contracts |
| Test gate | Unit tests in CI, optional coverage threshold | Shipping without safety net |
ESLint configuration sketch#
Keep it short and pragmatic. The exact config depends on your stack, but these are common essentials.
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
npm run lint - 2
npm run typecheck - 3
npm test - 4Build 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
Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.
More in Web Development
All →Next.js Multi‑Region Deployments: Patterns for Lower Latency on Vercel and Cloudflare
A practical guide to Next.js multi-region deployment in 2026: edge rendering, regional SSR, and data locality patterns on Vercel and Cloudflare, including database caveats and a latency validation checklist.
Next.js Monorepo with Turborepo + pnpm Workspaces: A Practical Setup Guide for 2026
Set up a production-ready Next.js monorepo using Turborepo and pnpm workspaces, with shared packages, linting, testing, and CI caching—plus the trade-offs and pitfalls to avoid.
React Accessibility Checklist: ARIA, Keyboard Navigation, Focus Management, and Testing (2026)
A developer-focused React accessibility checklist covering ARIA, keyboard navigation, focus management, and automated testing with axe and Playwright — with concrete examples for forms, modals, menus, and toasts.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
React Forms at Scale: React Hook Form + Zod Patterns for Complex Products
React forms best practices for large apps using React Hook Form and Zod: schema-first validation, reusable fields, async checks, multi-step flows, performance, accessibility, and server/API integration patterns.
React Accessibility Checklist: ARIA, Keyboard Navigation, Focus Management, and Testing (2026)
A developer-focused React accessibility checklist covering ARIA, keyboard navigation, focus management, and automated testing with axe and Playwright — with concrete examples for forms, modals, menus, and toasts.
React Table Virtualization & Infinite Scroll: Building Fast Data Grids with TanStack (2026 Guide)
Learn React table virtualization with TanStack Table, TanStack Virtual, and React Query: efficient rendering, infinite scroll, server-side sorting/filtering, URL sync, selection persistence, and optimistic updates.