React — Best Practices & Patterns
React is small on the surface — components, props, state, hooks — but production applications reveal a deep design space. The same library that lets you ship a todo list in an hour can power a team of fifty engineers building a streaming dashboard. Best practices are the bridge between those extremes.
This guide focuses on patterns that survive code review, scale across teams, and remain readable six months later. We cover component design, hooks discipline, state management, performance, security, testing, accessibility, and the move toward React Server Components. Each section includes practical examples you can apply today.
note
Before diving into patterns, a quick reference of the highest-leverage habits. Getting these right prevents the majority of performance bugs and maintenance issues in React codebases.
| Don't | Do Instead | Why |
|---|---|---|
| Index as key | Use stable, unique IDs | Prevents reordering and state mismatch bugs |
| Memoize everything | Profile first, memoize hot paths | Memo has overhead; blind use hurts more than helps |
| useEffect for derived state | Compute during render | Deriving avoids stale state and extra renders |
| Prop drilling deep trees | Composition or context | Keeps intermediate components decoupled |
| Mutating state directly | Replace state with new references | Immutability is required for React change detection |
| Calling hooks conditionally | Call hooks at the top level always | Preserves hook call order across renders |
| Large components | Split by responsibility | Easier to test, review, and reuse |
| any everywhere | Type props and state explicitly | Catches integration bugs at compile time |
| 1 | // Good — stable key, derived state, no mutation |
| 2 | import { useMemo } from "react"; |
| 3 | |
| 4 | type User = { id: string; name: string; role: "admin" | "user" }; |
| 5 | |
| 6 | function UserList({ users }: { users: User[] }) { |
| 7 | // Derived during render — no useEffect needed |
| 8 | const admins = useMemo( |
| 9 | () => users.filter((u) => u.role === "admin"), |
| 10 | [users] |
| 11 | ); |
| 12 | |
| 13 | return ( |
| 14 | <ul> |
| 15 | {admins.map((user) => ( |
| 16 | <li key={user.id}>{user.name}</li> {/* stable key */} |
| 17 | ))} |
| 18 | </ul> |
| 19 | ); |
| 20 | } |
| 21 | |
| 22 | // Bad — index key, mutation, effect-derived state |
| 23 | function UserListBad({ users }: { users: User[] }) { |
| 24 | const [admins, setAdmins] = React.useState<User[]>([]); |
| 25 | |
| 26 | React.useEffect(() => { |
| 27 | users.forEach((u) => { |
| 28 | if (u.role === "admin") { |
| 29 | admins.push(u); // mutates state directly |
| 30 | } |
| 31 | }); |
| 32 | setAdmins([...admins]); |
| 33 | }, [users]); |
| 34 | |
| 35 | return ( |
| 36 | <ul> |
| 37 | {admins.map((user, index) => ( |
| 38 | <li key={index}>{user.name}</li> {/* fragile key */} |
| 39 | ))} |
| 40 | </ul> |
| 41 | ); |
| 42 | } |
Consistent style removes friction in code review and lets developers focus on behavior. With TypeScript, conventions around naming, file structure, and types matter as much as formatting.
| 1 | // File: components/UserCard.tsx |
| 2 | import type { ReactNode } from "react"; |
| 3 | |
| 4 | // Props interface named after the component + "Props" |
| 5 | interface UserCardProps { |
| 6 | id: string; |
| 7 | name: string; |
| 8 | avatar?: string; |
| 9 | children?: ReactNode; |
| 10 | onEdit: (id: string) => void; |
| 11 | } |
| 12 | |
| 13 | // Function declaration for components (clearer hoisting behavior) |
| 14 | export function UserCard({ id, name, avatar, children, onEdit }: UserCardProps) { |
| 15 | return ( |
| 16 | <article className="user-card"> |
| 17 | {avatar ? <img src={avatar} alt={`Avatar of ${name}`} /> : null} |
| 18 | <h3>{name}</h3> |
| 19 | {children} |
| 20 | <button type="button" onClick={() => onEdit(id)}> |
| 21 | Edit |
| 22 | </button> |
| 23 | </article> |
| 24 | ); |
| 25 | } |
| 26 | |
| 27 | // Avoid default exports for components; prefer named exports. |
| 28 | // It makes refactoring and auto-imports more reliable. |
| 29 | |
| 30 | // Type event handlers narrowly instead of using any |
| 31 | function handleSubmit(event: React.FormEvent<HTMLFormElement>) { |
| 32 | event.preventDefault(); |
| 33 | } |
best practice
| 1 | // Hooks are functions prefixed with "use" and may return a tuple or object |
| 2 | import { useState, useCallback } from "react"; |
| 3 | |
| 4 | interface UseCounterReturn { |
| 5 | count: number; |
| 6 | increment: () => void; |
| 7 | decrement: () => void; |
| 8 | } |
| 9 | |
| 10 | export function useCounter(initial = 0): UseCounterReturn { |
| 11 | const [count, setCount] = useState(initial); |
| 12 | |
| 13 | const increment = useCallback(() => setCount((c) => c + 1), []); |
| 14 | const decrement = useCallback(() => setCount((c) => c - 1), []); |
| 15 | |
| 16 | return { count, increment, decrement }; |
| 17 | } |
| 18 | |
| 19 | // Use the hook |
| 20 | export function Counter() { |
| 21 | const { count, increment } = useCounter(10); |
| 22 | return <button onClick={increment}>Count: {count}</button>; |
| 23 | } |
A scalable React project groups related files together. Colocation beats separation by file type because features are added and removed as units. Keep components shallow, co-locate tests and styles, and isolate side effects at the edges of the system.
| 1 | my-app/ |
| 2 | src/ |
| 3 | app/ # Next.js App Router routes (or pages/) |
| 4 | layout.tsx |
| 5 | page.tsx |
| 6 | components/ |
| 7 | ui/ # low-level reusable primitives |
| 8 | Button/ |
| 9 | Button.tsx |
| 10 | Button.test.tsx |
| 11 | Button.module.css |
| 12 | index.ts |
| 13 | Card/ |
| 14 | Card.tsx |
| 15 | Card.test.tsx |
| 16 | features/ # domain-specific components |
| 17 | UserProfile/ |
| 18 | UserProfile.tsx |
| 19 | useUserProfile.ts |
| 20 | UserProfile.test.tsx |
| 21 | hooks/ # shared cross-cutting hooks |
| 22 | useLocalStorage.ts |
| 23 | useDebounce.ts |
| 24 | lib/ # pure utilities and config |
| 25 | api.ts |
| 26 | utils.ts |
| 27 | types/ # shared domain types |
| 28 | user.ts |
| 29 | styles/ |
| 30 | globals.css |
| 31 | public/ |
| 32 | package.json |
| 33 | tsconfig.json |
| 34 | eslint.config.mjs |
The boundary between components/ui and components/features is important. UI components know nothing about your business domain. Feature components can depend on UI components, but UI components should never import feature components.
info
Hooks are the core abstraction of modern React. The Rules of Hooks are not suggestions — React relies on their call order to associate state with components. Break them and you get subtle, hard-to-debug failures.
| 1 | // Good — hooks called at top level, stable dependencies |
| 2 | import { useEffect, useState } from "react"; |
| 3 | |
| 4 | function Search({ query }: { query: string }) { |
| 5 | const [results, setResults] = useState<string[]>([]); |
| 6 | |
| 7 | useEffect(() => { |
| 8 | const controller = new AbortController(); |
| 9 | |
| 10 | fetch(`/api/search?q=${encodeURIComponent(query)}`, { |
| 11 | signal: controller.signal, |
| 12 | }) |
| 13 | .then((res) => res.json()) |
| 14 | .then((data) => setResults(data.results)) |
| 15 | .catch((err) => { |
| 16 | if (err.name !== "AbortError") console.error(err); |
| 17 | }); |
| 18 | |
| 19 | return () => controller.abort(); |
| 20 | }, [query]); |
| 21 | |
| 22 | return ( |
| 23 | <ul> |
| 24 | {results.map((item) => ( |
| 25 | <li key={item}>{item}</li> |
| 26 | ))} |
| 27 | </ul> |
| 28 | ); |
| 29 | } |
| 30 | |
| 31 | // Bad — hook inside condition, missing cleanup |
| 32 | function SearchBad({ query }: { query: string }) { |
| 33 | if (query) { |
| 34 | // ❌ never call hooks conditionally |
| 35 | const [results, setResults] = useState([]); |
| 36 | } |
| 37 | |
| 38 | useEffect(() => { |
| 39 | fetch(`/api/search?q=${query}`) |
| 40 | .then((res) => res.json()) |
| 41 | .then(setResults); |
| 42 | // ❌ no cleanup and no dependency array |
| 43 | }); |
| 44 | } |
warning
Start with local state. Lift state only when a sibling needs it. Reach for context when many components at different nesting levels need the same data. Only add a global store when you have genuinely global, frequently accessed state or complex cross-cutting update logic.
| 1 | // Local state is enough here |
| 2 | function LikeButton() { |
| 3 | const [liked, setLiked] = useState(false); |
| 4 | return ( |
| 5 | <button onClick={() => setLiked((prev) => !prev)}> |
| 6 | {liked ? "♥" : "♡"} |
| 7 | </button> |
| 8 | ); |
| 9 | } |
| 10 | |
| 11 | // Lifted state for shared sibling data |
| 12 | function CounterParent() { |
| 13 | const [count, setCount] = useState(0); |
| 14 | return ( |
| 15 | <div> |
| 16 | <CounterDisplay count={count} /> |
| 17 | <CounterControls onChange={setCount} /> |
| 18 | </div> |
| 19 | ); |
| 20 | } |
| 21 | |
| 22 | // Context for theme/settings that many components consume |
| 23 | import { createContext, useContext, useState } from "react"; |
| 24 | |
| 25 | interface ThemeContextValue { |
| 26 | theme: "light" | "dark"; |
| 27 | toggle: () => void; |
| 28 | } |
| 29 | |
| 30 | const ThemeContext = createContext<ThemeContextValue | null>(null); |
| 31 | |
| 32 | export function ThemeProvider({ children }: { children: React.ReactNode }) { |
| 33 | const [theme, setTheme] = useState<"light" | "dark">("dark"); |
| 34 | const toggle = () => setTheme((t) => (t === "dark" ? "light" : "dark")); |
| 35 | return ( |
| 36 | <ThemeContext.Provider value={{ theme, toggle }}> |
| 37 | {children} |
| 38 | </ThemeContext.Provider> |
| 39 | ); |
| 40 | } |
| 41 | |
| 42 | export function useTheme() { |
| 43 | const ctx = useContext(ThemeContext); |
| 44 | if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); |
| 45 | return ctx; |
| 46 | } |
Keep state as low in the tree as possible. State placed too high causes unnecessary re-renders and couples unrelated components. Similarly, avoid duplicating state that can be derived from props or existing state.
React is fast by default. Most performance problems come from unnecessary renders, large lists, or expensive computations running on every render. Measure before optimizing. The React DevTools Profiler is your first stop.
| 1 | import { memo, useMemo, useCallback } from "react"; |
| 2 | |
| 3 | interface ExpensiveListProps { |
| 4 | items: { id: string; name: string; value: number }[]; |
| 5 | onSelect: (id: string) => void; |
| 6 | } |
| 7 | |
| 8 | // Memoize a child whose parent re-renders often |
| 9 | const ListItem = memo(function ListItem({ |
| 10 | item, |
| 11 | onSelect, |
| 12 | }: { |
| 13 | item: ExpensiveListProps["items"][number]; |
| 14 | onSelect: (id: string) => void; |
| 15 | }) { |
| 16 | return ( |
| 17 | <li> |
| 18 | <button onClick={() => onSelect(item.id)}> |
| 19 | {item.name}: {item.value} |
| 20 | </button> |
| 21 | </li> |
| 22 | ); |
| 23 | }); |
| 24 | |
| 25 | export function ExpensiveList({ items, onSelect }: ExpensiveListProps) { |
| 26 | // Memoize expensive derived data |
| 27 | const total = useMemo( |
| 28 | () => items.reduce((sum, item) => sum + item.value, 0), |
| 29 | [items] |
| 30 | ); |
| 31 | |
| 32 | // Stable callback so memoized children don't re-render unnecessarily |
| 33 | const handleSelect = useCallback( |
| 34 | (id: string) => onSelect(id), |
| 35 | [onSelect] |
| 36 | ); |
| 37 | |
| 38 | return ( |
| 39 | <div> |
| 40 | <p>Total: {total}</p> |
| 41 | <ul> |
| 42 | {items.map((item) => ( |
| 43 | <ListItem key={item.id} item={item} onSelect={handleSelect} /> |
| 44 | ))} |
| 45 | </ul> |
| 46 | </div> |
| 47 | ); |
| 48 | } |
info
| 1 | // Virtualize long lists instead of rendering all rows |
| 2 | import { FixedSizeList as List } from "react-window"; |
| 3 | |
| 4 | function VirtualizedList({ items }: { items: string[] }) { |
| 5 | const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => ( |
| 6 | <div style={style}>{items[index]}</div> |
| 7 | ); |
| 8 | |
| 9 | return ( |
| 10 | <List |
| 11 | height={400} |
| 12 | itemCount={items.length} |
| 13 | itemSize={35} |
| 14 | width="100%" |
| 15 | > |
| 16 | {Row} |
| 17 | </List> |
| 18 | ); |
| 19 | } |
| 20 | |
| 21 | // Lazy load routes and heavy components to reduce initial bundle size |
| 22 | import { lazy, Suspense } from "react"; |
| 23 | |
| 24 | const Dashboard = lazy(() => import("./Dashboard")); |
| 25 | |
| 26 | function App() { |
| 27 | return ( |
| 28 | <Suspense fallback={<Spinner />}> |
| 29 | <Dashboard /> |
| 30 | </Suspense> |
| 31 | ); |
| 32 | } |
Composition is React's superpower. It lets you build flexible APIs without prop explosion, and it keeps components decoupled. Prefer children and slots over configuration objects when the consumer needs control over rendered output.
| 1 | // Flexible layout with composition instead of many props |
| 2 | import type { ReactNode } from "react"; |
| 3 | |
| 4 | interface CardProps { |
| 5 | title: string; |
| 6 | children: ReactNode; |
| 7 | actions?: ReactNode; |
| 8 | } |
| 9 | |
| 10 | export function Card({ title, children, actions }: CardProps) { |
| 11 | return ( |
| 12 | <div className="card"> |
| 13 | <h2>{title}</h2> |
| 14 | <div className="card-body">{children}</div> |
| 15 | {actions ? <div className="card-actions">{actions}</div> : null} |
| 16 | </div> |
| 17 | ); |
| 18 | } |
| 19 | |
| 20 | // Usage |
| 21 | <Card |
| 22 | title="Project Settings" |
| 23 | actions={ |
| 24 | <> |
| 25 | <button>Cancel</button> |
| 26 | <button>Save</button> |
| 27 | </> |
| 28 | } |
| 29 | > |
| 30 | <p>Configure your project here.</p> |
| 31 | </Card> |
| 32 | |
| 33 | // Compound components for tightly related pieces |
| 34 | import { createContext, useContext, useState } from "react"; |
| 35 | |
| 36 | const TabsContext = createContext<{ |
| 37 | active: string; |
| 38 | setActive: (value: string) => void; |
| 39 | } | null>(null); |
| 40 | |
| 41 | export function Tabs({ |
| 42 | defaultTab, |
| 43 | children, |
| 44 | }: { |
| 45 | defaultTab: string; |
| 46 | children: ReactNode; |
| 47 | }) { |
| 48 | const [active, setActive] = useState(defaultTab); |
| 49 | return ( |
| 50 | <TabsContext.Provider value={{ active, setActive }}> |
| 51 | {children} |
| 52 | </TabsContext.Provider> |
| 53 | ); |
| 54 | } |
| 55 | |
| 56 | Tabs.List = function List({ children }: { children: ReactNode }) { |
| 57 | return <div role="tablist">{children}</div>; |
| 58 | }; |
| 59 | |
| 60 | Tabs.Tab = function Tab({ value, children }: { value: string; children: ReactNode }) { |
| 61 | const ctx = useContext(TabsContext); |
| 62 | if (!ctx) throw new Error("Tab must be inside Tabs"); |
| 63 | return ( |
| 64 | <button |
| 65 | role="tab" |
| 66 | aria-selected={ctx.active === value} |
| 67 | onClick={() => ctx.setActive(value)} |
| 68 | > |
| 69 | {children} |
| 70 | </button> |
| 71 | ); |
| 72 | }; |
| 73 | |
| 74 | Tabs.Panel = function Panel({ value, children }: { value: string; children: ReactNode }) { |
| 75 | const ctx = useContext(TabsContext); |
| 76 | if (!ctx) throw new Error("Panel must be inside Tabs"); |
| 77 | return ctx.active === value ? <div role="tabpanel">{children}</div> : null; |
| 78 | }; |
React Server Components (RSC) move data fetching and heavy rendering to the server. They are not a replacement for client components; they are a new primitive for building applications. In frameworks like Next.js, components are server components by default in the App Router.
| 1 | // Server Component — fetches data directly, zero client JS |
| 2 | async function PostList() { |
| 3 | const posts = await fetch("https://api.example.com/posts", { |
| 4 | cache: "force-cache", |
| 5 | }).then((res) => res.json()); |
| 6 | |
| 7 | return ( |
| 8 | <ul> |
| 9 | {posts.map((post: { id: string; title: string }) => ( |
| 10 | <li key={post.id}>{post.title}</li> |
| 11 | ))} |
| 12 | </ul> |
| 13 | ); |
| 14 | } |
| 15 | |
| 16 | // Client Component for interactivity |
| 17 | "use client"; |
| 18 | |
| 19 | import { useState } from "react"; |
| 20 | |
| 21 | function LikeButton({ postId }: { postId: string }) { |
| 22 | const [liked, setLiked] = useState(false); |
| 23 | |
| 24 | return ( |
| 25 | <button onClick={() => setLiked((prev) => !prev)}> |
| 26 | {liked ? "Unlike" : "Like"} {postId} |
| 27 | </button> |
| 28 | ); |
| 29 | } |
| 30 | |
| 31 | // Compose server and client components; pass server data as props |
| 32 | async function PostPage({ params }: { params: { id: string } }) { |
| 33 | const post = await fetch(`https://api.example.com/posts/${params.id}`).then( |
| 34 | (res) => res.json() |
| 35 | ); |
| 36 | |
| 37 | return ( |
| 38 | <article> |
| 39 | <h1>{post.title}</h1> |
| 40 | <p>{post.body}</p> |
| 41 | <LikeButton postId={post.id} /> |
| 42 | </article> |
| 43 | ); |
| 44 | } |
best practice
React provides some XSS protection by escaping interpolated values, but applications still face injection risks through dangerouslySetInnerHTML, unsafe URLs, unvalidated form submissions, and leaked secrets in client bundles.
| 1 | // Dangerous — only use when content is fully trusted and sanitized |
| 2 | function SafeHtml({ html }: { html: string }) { |
| 3 | // html must be sanitized by DOMPurify or similar on the server |
| 4 | return <div dangerouslySetInnerHTML={{ __html: html }} />; |
| 5 | } |
| 6 | |
| 7 | // Safe — React escapes text automatically |
| 8 | function UserComment({ text }: { text: string }) { |
| 9 | return <p>{text}</p>; // <script> tags are rendered as text, not executed |
| 10 | } |
| 11 | |
| 12 | // Validate URLs before rendering them |
| 13 | function ExternalLink({ href, children }: { href: string; children: React.ReactNode }) { |
| 14 | const isSafe = href.startsWith("https://") || href.startsWith("/"); |
| 15 | if (!isSafe) return null; |
| 16 | return ( |
| 17 | <a href={href} target="_blank" rel="noopener noreferrer"> |
| 18 | {children} |
| 19 | </a> |
| 20 | ); |
| 21 | } |
| 22 | |
| 23 | // Never put secrets in client bundles |
| 24 | // ❌ Bad: Vite exposes env vars prefixed with VITE_ to the client |
| 25 | const API_KEY = import.meta.env.VITE_SECRET_API_KEY; // visible in bundle |
| 26 | |
| 27 | // ✅ Good: keep secrets in server components, route handlers, or env files |
| 28 | // loaded only by the server runtime |
| 29 | async function ServerData() { |
| 30 | const data = await fetch("https://api.example.com/data", { |
| 31 | headers: { Authorization: `Bearer ${process.env.API_KEY}` }, |
| 32 | }); |
| 33 | return <pre>{JSON.stringify(await data.json(), null, 2)}</pre>; |
| 34 | } |
warning
Test behavior, not implementation. React Testing Library encourages tests that resemble how users interact with your application. Avoid testing internal state or asserting on exact component structure; those tests break during refactoring.
| 1 | import { render, screen, fireEvent } from "@testing-library/react"; |
| 2 | import { Counter } from "./Counter"; |
| 3 | |
| 4 | test("increments count when button is clicked", () => { |
| 5 | render(<Counter />); |
| 6 | |
| 7 | const button = screen.getByRole("button", { name: /count/i }); |
| 8 | expect(button).toHaveTextContent("Count: 0"); |
| 9 | |
| 10 | fireEvent.click(button); |
| 11 | expect(button).toHaveTextContent("Count: 1"); |
| 12 | }); |
| 13 | |
| 14 | // Custom hook test with renderHook |
| 15 | import { renderHook, act } from "@testing-library/react"; |
| 16 | import { useCounter } from "./useCounter"; |
| 17 | |
| 18 | test("useCounter increments and decrements", () => { |
| 19 | const { result } = renderHook(() => useCounter(5)); |
| 20 | |
| 21 | act(() => result.current.increment()); |
| 22 | expect(result.current.count).toBe(6); |
| 23 | |
| 24 | act(() => result.current.decrement()); |
| 25 | expect(result.current.count).toBe(5); |
| 26 | }); |
| 27 | |
| 28 | // MSW for network mocking |
| 29 | import { http, HttpResponse } from "msw"; |
| 30 | import { server } from "./mocks/server"; |
| 31 | |
| 32 | test("displays user after loading", async () => { |
| 33 | server.use( |
| 34 | http.get("/api/user", () => { |
| 35 | return HttpResponse.json({ name: "Ada" }); |
| 36 | }) |
| 37 | ); |
| 38 | |
| 39 | render(<UserProfile userId="1" />); |
| 40 | expect(await screen.findByText("Ada")).toBeInTheDocument(); |
| 41 | }); |
info
Accessible React apps are better for everyone and required by law in many jurisdictions. Most accessibility issues are caused by incorrect markup, missing labels, or keyboard traps rather than React itself. Use semantic HTML and manage focus deliberately.
| 1 | // Accessible form with labels and error messages |
| 2 | function LoginForm() { |
| 3 | return ( |
| 4 | <form aria-label="Login form"> |
| 5 | <div> |
| 6 | <label htmlFor="email">Email</label> |
| 7 | <input |
| 8 | id="email" |
| 9 | type="email" |
| 10 | name="email" |
| 11 | aria-required="true" |
| 12 | aria-describedby="email-error" |
| 13 | /> |
| 14 | <span id="email-error" role="alert"> |
| 15 | Invalid email |
| 16 | </span> |
| 17 | </div> |
| 18 | <button type="submit">Sign in</button> |
| 19 | </form> |
| 20 | ); |
| 21 | } |
| 22 | |
| 23 | // Use useId for stable unique IDs in reusable components |
| 24 | import { useId } from "react"; |
| 25 | |
| 26 | function TextField({ label }: { label: string }) { |
| 27 | const id = useId(); |
| 28 | return ( |
| 29 | <div> |
| 30 | <label htmlFor={id}>{label}</label> |
| 31 | <input id={id} type="text" /> |
| 32 | </div> |
| 33 | ); |
| 34 | } |
| 35 | |
| 36 | // Manage focus after actions, especially modal open/close |
| 37 | import { useRef, useEffect } from "react"; |
| 38 | |
| 39 | function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { |
| 40 | const ref = useRef<HTMLDialogElement>(null); |
| 41 | |
| 42 | useEffect(() => { |
| 43 | if (isOpen) ref.current?.showModal(); |
| 44 | else ref.current?.close(); |
| 45 | }, [isOpen]); |
| 46 | |
| 47 | return ( |
| 48 | <dialog ref={ref} onClose={onClose}> |
| 49 | <button onClick={onClose}>Close</button> |
| 50 | </dialog> |
| 51 | ); |
| 52 | } |
best practice
A modern React project benefits from a toolchain that catches errors early and enforces consistency. TypeScript, ESLint, and Prettier are the baseline. Add testing, bundle analysis, and CI checks as the project matures.
| 1 | // tsconfig.json — strict mode recommended for React projects |
| 2 | { |
| 3 | "compilerOptions": { |
| 4 | "target": "ES2022", |
| 5 | "lib": ["dom", "dom.iterable", "esnext"], |
| 6 | "allowJs": false, |
| 7 | "skipLibCheck": true, |
| 8 | "strict": true, |
| 9 | "noUncheckedIndexedAccess": true, |
| 10 | "exactOptionalPropertyTypes": true, |
| 11 | "forceConsistentCasingInFileNames": true, |
| 12 | "module": "ESNext", |
| 13 | "moduleResolution": "bundler", |
| 14 | "resolveJsonModule": true, |
| 15 | "isolatedModules": true, |
| 16 | "jsx": "preserve", |
| 17 | "incremental": true, |
| 18 | "paths": { |
| 19 | "@/*": ["./src/*"] |
| 20 | } |
| 21 | }, |
| 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], |
| 23 | "exclude": ["node_modules"] |
| 24 | } |
| 1 | // package.json — recommended scripts and dependencies |
| 2 | { |
| 3 | "scripts": { |
| 4 | "dev": "next dev", |
| 5 | "build": "next build", |
| 6 | "start": "next start", |
| 7 | "lint": "next lint", |
| 8 | "typecheck": "tsc --noEmit", |
| 9 | "test": "vitest", |
| 10 | "test:e2e": "playwright test", |
| 11 | "analyze": "cross-env ANALYZE=true next build" |
| 12 | }, |
| 13 | "devDependencies": { |
| 14 | "@testing-library/react": "^16.x", |
| 15 | "@types/react": "^19.x", |
| 16 | "eslint": "^9.x", |
| 17 | "eslint-config-next": "latest", |
| 18 | "prettier": "^3.x", |
| 19 | "typescript": "^5.x", |
| 20 | "vitest": "^3.x" |
| 21 | } |
| 22 | } |
info
A structured review checklist keeps quality consistent across a team. Automate the easy parts with CI; reserve human review for architecture, correctness, and user experience.
| Check | Automated? | Details |
|---|---|---|
| Type check | tsc --noEmit | No implicit any, props typed |
| Lint | eslint | Rules of hooks, exhaustive deps |
| Format | prettier | Consistent code style |
| Tests | vitest / jest | New behavior covered |
| Keys | eslint / review | Stable IDs, not array index |
| Hook deps | eslint-plugin-react-hooks | Dependency arrays correct |
| Accessibility | axe / review | Labels, roles, keyboard flow |
| Security | review | No secrets, sanitized HTML |
| Performance | profiler / review | Memoization justified |
| Component size | review | Single responsibility |
| 1 | # .github/workflows/ci.yml example |
| 2 | name: CI |
| 3 | on: [push, pull_request] |
| 4 | jobs: |
| 5 | check: |
| 6 | runs-on: ubuntu-latest |
| 7 | steps: |
| 8 | - uses: actions/checkout@v4 |
| 9 | - uses: actions/setup-node@v4 |
| 10 | with: |
| 11 | node-version: 22 |
| 12 | cache: npm |
| 13 | - run: npm ci |
| 14 | - run: npm run lint |
| 15 | - run: npm run typecheck |
| 16 | - run: npm test -- --run |
| 17 | - run: npm run build |
The React ecosystem evolves quickly. Refer to the official sources for authoritative guidance and keep your dependencies current without chasing every release.
| Resource | What it covers |
|---|---|
| react.dev | Official docs, hooks, thinking in React, RSC |
| React TypeScript Cheatsheet | Common React + TS patterns and gotchas |
| Testing Library docs | Guiding principles and query priorities |
| WCAG 2.2 | Web Content Accessibility Guidelines |
| Next.js docs | App Router, caching, server components |