Overview
React 19 sharpens the patterns that already worked and removes a few that did not. Server components are stable, Suspense covers data and transitions, and the compiler removes most reasons to write memoization by hand. 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.
<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.
// 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.
// 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.
Skip useMemo and useCallback until you measure
The React 19 compiler memoizes most of what teams used to write by hand. Reach for useMemo or useCallback 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:
- 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.
<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.
Server components by default; client components when needed
In a React 19 framework (Next.js 15, 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 nextjs for the framework specifics.
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.
// 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.