---
title: "React Best Practices"
slug: "react"
category: "frontend"
tags: ["frontend", "react", "hooks", "components", "suspense"]
status: "stable"
last_updated: 2026-05-29
summary: "React 19 patterns: composition over inheritance, hook discipline, state colocation, Suspense, server vs client components."
related: ["[[frontend/nextjs]]", "[[frontend/shadcn]]", "[[frontend/tailwind]]", "[[frontend/react-hooks]]", "[[frontend/react-server-components]]", "[[frontend/react-suspense]]", "[[frontend/react-state-management]]", "[[frontend/react-compiler]]", "[[coding/typescript]]"]
---

## Overview

React 19 sharpens the patterns that already worked and removes a few that did not. Server components are stable and Suspense covers data and transitions. The [[frontend/react-compiler|React Compiler]] (stable as of v1.0) removes most reasons to write memoization by hand, but it is an opt-in build plugin, not on by default in a React 19 app. This page is the rule set for a React 19 codebase in 2026.

## Compose components, do not inherit

React has no inheritance model worth using. Build with composition: small components, `children` as the extension point, prop-driven variants. Pair this with semantic HTML and the keyboard rules in [[frontend/accessibility]] so the composed tree stays operable.

```tsx
<Card>
  <Card.Header>Title</Card.Header>
  <Card.Body><Stats data={data} /></Card.Body>
</Card>

<Button variant="primary" size="sm">Save</Button>
```

When two components share logic, extract a hook or a utility, not a base component.

## Obey the rules of hooks

Hooks run only at the top level of a function component or another hook. Never inside a condition, a loop, or after an early `return`. Never inside an event handler or a class. For the deeper rules on `useState`, `useReducer`, `useEffect`, and custom hook naming, see [[frontend/react-hooks]].

```tsx
// Wrong: conditional hook.
if (open) {
  useEffect(() => track("open"), []);
}

// Right: condition lives inside the effect.
useEffect(() => {
  if (open) track("open");
}, [open]);
```

Run ESLint with `eslint-plugin-react-hooks`. The `exhaustive-deps` rule is right almost always; silencing it should be rare and commented.

## Colocate state, lift only when shared

Put state in the component that uses it. Lift state up only when two siblings need to read or write the same value. Do not pre-emptively hoist state to a parent "in case." A Context that wraps the whole app and stores a single component's state is a smell.

```tsx
// Local: only this component cares.
function SearchBox() {
  const [query, setQuery] = useState("");
  // ...
}
```

For genuinely global state (auth, theme, current org), use Context with a small reducer or a focused store (`zustand`). Avoid putting server data in client state; fetch it through Suspense or a query library. See [[frontend/react-state-management]] for the rule of three on when to lift.

## Skip `useMemo` and `useCallback` until you measure

When the [[frontend/react-compiler|React Compiler]] is enabled, it memoizes most of what teams used to write by hand, so `useMemo` and `useCallback` become rare. The compiler is opt-in: a plain React 19 app does not get automatic memoization until you turn the plugin on. With or without it, reach for manual memoization only when a profiler shows the cost; a `useMemo` wrapping a cheap expression slows the app down by adding bookkeeping.

Cases where manual memoization still pays (and the cases the compiler cannot reach):

- The value is a stable reference passed to a `memo()` child whose render is expensive.
- The computation is genuinely heavy (parsing, large array transforms).
- The dependency array is small and stable.

Outside those cases, write the plain expression.

## Suspense for data, transitions for stale UI

Use Suspense to express loading boundaries declaratively. Inside a Suspense boundary, a component can read data with `use()` and the boundary handles the fallback.

```tsx
<Suspense fallback={<Skeleton />}>
  <UserProfile id={id} />
</Suspense>
```

For navigations and filters that should not flash a spinner, wrap the update in `startTransition` or `useTransition`. The UI stays interactive on the old data until the new data is ready. See [[frontend/react-suspense]] for the full boundary and streaming playbook.

## Server components by default; client components when needed

In a React 19 framework (Next.js, Remix, Waku), components render on the server unless you mark them `"use client"`. Server components ship zero JS to the browser and can fetch data directly.

Mark a file `"use client"` when it needs:

- `useState`, `useEffect`, `useReducer`, refs, browser APIs.
- Event handlers (`onClick`, `onChange`).
- Third-party client-only libraries (charts, editors, maps).

Keep the boundary thin. A client island wraps the interactive bit; everything outside it stays on the server. See [[frontend/react-server-components]] for the serialization and `use client` rules, [[frontend/nextjs]] for the framework specifics, [[comparisons/server-vs-client-components]] for the head-to-head selection rules, [[frontend/forms]] for the form submission patterns that survive without JS on the client, and [[cheatsheets/tsx-jsx-syntax]] for a quick-reference on JSX syntax and TypeScript component signatures.

## Never `useEffect` for derived state

If a value can be computed from props or existing state, compute it during render. Do not mirror props into state with `useEffect`. Each effect is a re-render plus a re-render.

```tsx
// Wrong: effect mirrors props into state.
const [fullName, setFullName] = useState("");
useEffect(() => {
  setFullName(`${first} ${last}`);
}, [first, last]);

// Right: derive during render.
const fullName = `${first} ${last}`;
```

The same rule applies to filtering, sorting, and formatting. Compute in render; memoize only when the profiler asks for it.

## Related

- [[frontend/nextjs]]
- [[frontend/shadcn]]
- [[frontend/tailwind]]
- [[frontend/react-compiler]]
- [[coding/typescript]]
