languages/javascript/frameworks/frontend/react_best_practices.md

React Best Practices (2026)

Production-grade React guidance for 2026: the Rules of Hooks, Server Components vs Client Components, the modern state management landscape (Zustand, TanStack Query, Redux Toolkit), and common rendering performance pitfalls.

Introduction / Overview

React 19 is stable and enterprise-production-ready in 2026, and the features that were experimental in React 18 - Server Components and Actions - are now standardized. Next.js's App Router, which uses React Server Components (RSC) by default, has overtaken the Pages Router as the most common React setup according to recent State of JavaScript data. At the same time, client-side state management has settled into a clear division of labor rather than a single winning library.

This document covers the mental models and practices that matter most for teams shipping React applications in 2026: correct Hook usage, the Server/Client Component boundary, a pragmatic state management stack, and the rendering-performance pitfalls that recur across almost every non-trivial React codebase.

When to use this guidance: Building a new React application (with or without a Server Components-capable framework), reviewing component architecture, or diagnosing unnecessary re-renders and performance regressions.

Core Concepts

  • Rules of Hooks: Two invariants - call Hooks only at the top level (never inside conditionals, loops, or nested functions), and call Hooks only from React function components or custom Hooks. These rules exist because React tracks Hook state by call order, not by name.
  • Server Components (RSC): Components that render exclusively on the server, never ship their code to the client bundle, and can directly access server-only resources (databases, filesystem, secrets) without an API layer. They cannot use state, effects, or browser-only APIs.
  • Client Components: Components marked "use client" that hydrate and run in the browser, supporting interactivity, state, and effects. In an RSC-enabled framework, Client Components are the exception, opted into deliberately, not the default.
  • Composition over configuration for state: Modern React state management is not "pick one library" but "pick the right tool per category of state" - server state, global client state, and local component state each have a best-fit solution.
  • Reconciliation and re-renders: React re-renders a component whenever its state, props, or context change. Understanding why a component re-rendered (via React DevTools' profiler) is the prerequisite to fixing any performance problem, before reaching for memo or useMemo.

Best Practices

1. Follow the Rules of Hooks and Enforce Them With Lint

Rationale: Violating the Rules of Hooks (e.g., calling useState inside an if) causes React to associate the wrong state slot with the wrong Hook call on a subsequent render, producing bugs that are difficult to reproduce and debug.

// Wrong: conditional Hook call
function UserBadge({ userId }: { userId: string | null }) {
  if (!userId) return null
  const [user] = useUser(userId) // violates Rules of Hooks
  return <span>{user.name}</span>
}

// Correct: Hook always called, condition applied to its result
function UserBadge({ userId }: { userId: string | null }) {
  const [user] = useUser(userId ?? "")
  if (!userId || !user) return null
  return <span>{user.name}</span>
}
// eslint.config.js - non-negotiable for any React codebase
{ "plugins": { "react-hooks": reactHooks }, "rules": { "react-hooks/rules-of-hooks": "error" } }

2. Default to Server Components; Opt Into Client Components Deliberately

Rationale: Every line of a Server Component's code stays on the server - smaller client bundles, faster first paint, and direct, secure access to backend resources without hand-rolling an API endpoint. "use client" should mark the smallest possible subtree that genuinely needs interactivity.

// app/orders/[id]/page.tsx - Server Component (no "use client")
import { OrderActions } from "./order-actions"
import { db } from "@/lib/db"

export default async function OrderPage({ params }: { params: { id: string } }) {
  const order = await db.order.findUnique({ where: { id: params.id } }) // direct DB access, no API layer
  return (
    <article>
      <h1>Order {order.id}</h1>
      <OrderActions orderId={order.id} status={order.status} />
    </article>
  )
}
// app/orders/[id]/order-actions.tsx - Client Component, interactivity only
"use client"

import { useState } from "react"

export function OrderActions({ orderId, status }: { orderId: string; status: string }) {
  const [pending, setPending] = useState(false)
  return (
    <button disabled={pending} onClick={() => setPending(true)}>
      {pending ? "Cancelling..." : "Cancel Order"}
    </button>
  )
}

Why this works well in production: Data fetching happens where the data lives, eliminating a round trip through a client-side fetch and a separate loading state for the initial page render. As of 2026, Next.js is the only framework with full production-ready RSC support; other meta-frameworks are adopting it incrementally.

3. Split State by Category and Use the Matching Tool

Rationale: Treating all state uniformly (e.g., putting server data into Redux, or prop-drilling global UI state) creates unnecessary boilerplate and stale-cache bugs. The 2026 consensus stack pairs a dedicated server-state library with a minimal client-state store.

ToolState categoryBundle sizeWhen to choose
TanStack QueryServer state (fetched, cached, revalidated data)~13 KBAlways, for any data that originates from a server - handles caching, refetching, retries, and race conditions out of the box
ZustandGlobal client-only state (UI state, feature flags, selections)~1.2 KBDefault choice for cross-component client state that isn't tied to a server resource
Redux ToolkitComplex, highly structured client state with strict action auditing~6-8 KBLarge teams needing time-travel debugging, strict action logging, or an existing Redux investment
React useState/useReducer + ContextLocal or narrowly-shared component state0 KB (built-in)Default for state that does not need to escape a component subtree
// TanStack Query: server state, with caching and revalidation handled for you
function useOrder(orderId: string) {
  return useQuery({
    queryKey: ["order", orderId],
    queryFn: () => fetch(`/api/orders/${orderId}`).then((r) => r.json()),
    staleTime: 30_000,
  })
}

// Zustand: minimal global client state
import { create } from "zustand"

interface UiState {
  sidebarOpen: boolean
  toggleSidebar: () => void
}

const useUiStore = create<UiState>((set) => ({
  sidebarOpen: true,
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}))

Why this works well in production: Most applications need TanStack Query plus Zustand plus URL state (via the router) and nothing more; reaching for Redux by default adds boilerplate the application does not need.

4. Measure Before Memoizing

Rationale: React.memo, useMemo, and useCallback all have a cost (comparison overhead, closure allocation) and only pay for themselves when they prevent an expensive re-render. Applying them reflexively across a codebase adds complexity without measurable benefit and can even mask correctness bugs (stale closures).

// Only memoize after profiling shows this component re-renders expensively and often
const ExpensiveChart = memo(function ExpensiveChart({ data }: { data: ChartPoint[] }) {
  return <Chart points={data} />
})

// useMemo only for genuinely expensive derived computation
const sortedRows = useMemo(() => rows.toSorted((a, b) => a.total - b.total), [rows])

Why this works well in production: Use the React DevTools Profiler to identify components that re-render frequently and are expensive to render, then memoize only those. React's compiler (React Compiler, stable alongside React 19) automatically applies much of this memoization at build time, reducing the need for manual useMemo/useCallback in codebases that adopt it.

5. Keep Components Small and Colocate State With Its Usage

Rationale: A large component with many useState calls tends to re-render its entire subtree on every state change, even parts unrelated to what changed. Splitting into smaller components with narrowly-scoped state limits the blast radius of a re-render.

// Before: one state change re-renders the entire form, including unrelated fields
function CheckoutForm() {
  const [email, setEmail] = useState("")
  const [couponCode, setCouponCode] = useState("")
  // every keystroke in either field re-renders both
}

// After: isolate volatile state to the smallest component that needs it
function CheckoutForm() {
  return (
    <>
      <EmailField />
      <CouponField />
    </>
  )
}

Common Pitfalls & Anti-Patterns

Pitfall: Marking Entire Route Trees "use client" Out of Habit

Problem: Adding "use client" at a high-level layout or page component pulls every descendant into the client bundle, defeating the purpose of Server Components and increasing bundle size and hydration cost.

Recommended approach: Push "use client" as far down the tree as possible - ideally onto small leaf components that genuinely need useState, effects, or event handlers (Best Practice #2).

Pitfall: Storing Server Data in Global Client State

Problem: Copying fetch results into Redux or Zustand duplicates TanStack Query's caching/revalidation logic manually, and the copy silently goes stale.

Recommended approach: Use TanStack Query (or the framework's built-in data cache for Server Components) as the single source of truth for server-originated data; reserve Zustand/Redux for state that has no server origin.

Pitfall: Creating New Objects/Functions Inline as Props Without Considering Re-render Impact

Problem: <Child config={{ theme: "dark" }} onClick={() => doThing()} /> creates a new object and function identity on every render, defeating React.memo on Child even when nothing meaningful changed.

Recommended approach: Hoist stable values outside the render function, or wrap with useMemo/useCallback only where a profiled memo-wrapped child is genuinely affected.

Pitfall: Overusing useEffect for Derived State

Problem: Syncing one piece of state into another via useEffect (useEffect(() => setFullName(first + " " + last), [first, last])) adds an extra render pass and a source of bugs when dependencies are incomplete.

Recommended approach: Compute derived values directly during render (const fullName = \${first} ${last}\``) instead of storing and syncing them in separate state.

Testing Strategies

  • Component testing: Use @testing-library/react with Vitest, querying by accessible role/text rather than implementation details (test IDs as a last resort).
  • Server Component testing: Test the async data-fetching logic as a plain async function independent of JSX where possible; use framework-provided test harnesses (e.g., Next.js's testing utilities) for full route rendering.
  • Hook testing: Use @testing-library/react's renderHook for custom Hooks in isolation, especially state-management Hooks built on Zustand or TanStack Query.
  • End-to-end: Playwright for full user flows spanning Server and Client Component boundaries, verifying hydration completes and interactivity works as expected.

Recommended test stack: Vitest + @testing-library/react + Playwright for E2E, with TanStack Query's QueryClientProvider wrapped in a fresh, isolated QueryClient per test.

Performance, Security & Scalability Considerations

  • Bundle size: Server Components ship zero client JavaScript for their own code; auditing which components truly need "use client" is the single highest-leverage bundle-size lever available in an RSC-enabled app.
  • Waterfalls: Sequential await calls inside a Server Component create request waterfalls; use Promise.all to parallelize independent data fetches.
  • XSS: React escapes interpolated content by default; the only common injection vector is dangerouslySetInnerHTML, which must be paired with a sanitizer (e.g., DOMPurify) for any non-trusted input.
  • Secrets: Server Components can safely read environment secrets and query databases directly; never pass secret values as props into a Client Component, since they would be serialized into the client bundle/payload.
  • List rendering at scale: Use windowing (@tanstack/react-virtual) for lists beyond a few hundred rows to avoid mounting excessive DOM nodes.

Edge Cases & Advanced Usage

  • Streaming with Suspense: Wrap slow Server Component data fetches in <Suspense fallback={...}> to stream the shell immediately and progressively reveal slower sections, rather than blocking the entire page on the slowest query.
  • Server Actions: Functions marked "use server" allow Client Components to invoke server-side mutations directly (form submissions, data writes) without hand-writing a REST/GraphQL endpoint - useful, but still an evolving pattern with framework-specific caveats around caching and revalidation.
  • Hydration mismatches: Rendering non-deterministic values (current time, random IDs, window-dependent logic) during the server render but differently on the client causes hydration errors; guard such logic with a client-only effect or a stable seed.
  • Migrating a Pages Router app to the App Router/RSC: Migrate incrementally, route by route, keeping data-fetching logic in Server Components while porting interactive islands to explicitly-marked Client Components.

When Not to Use / Alternatives

DimensionReact (RSC-enabled)React (SPA, client-only)Svelte/SolidJS
SEO / first paintExcellentRequires extra work (SSR bolted on)Excellent
Ecosystem size (2026)LargestLargestSmaller, growing
Learning curveModerate-high (Server/Client boundary)ModerateLower (less boilerplate)
Fine-grained reactivityCompiler-assisted, not nativeN/ANative (Solid), compiler-assisted (Svelte)
Best fitContent-heavy apps needing SEO + interactivityInternal tools, dashboards behind authTeams prioritizing minimal runtime and bundle size

Choose a client-only React SPA for internal tools where SEO is irrelevant and the added complexity of the Server/Client boundary is not worth it. Choose Svelte or SolidJS when bundle size and raw runtime performance outweigh the value of React's larger ecosystem and hiring pool.

Glossary

  • Rules of Hooks: The invariant that Hooks must be called unconditionally, at the top level, only from components or custom Hooks.
  • Server Component (RSC): A component rendered exclusively on the server, contributing no JavaScript to the client bundle.
  • Client Component: A component marked "use client" that hydrates and executes in the browser.
  • Hydration: The process by which React attaches event listeners and interactivity to server-rendered HTML in the browser.
  • Server Action: A function marked "use server" that a Client Component can invoke to perform a server-side mutation.
  • Reconciliation: React's algorithm for determining what changed between renders and updating the DOM minimally.

Maintenance note: When updating this document, also update last_updated and re-verify RSC framework support beyond Next.js, since adoption is actively evolving across the React ecosystem.