lh-react-next · v1.1 · 2026-10-19 · sha256 b4a59f95c1a1dc5f

lh-react-next v1.1A

Immutable. This exact content never changes and is served at /api/v1/blob/b4a59f95c1a1dc5f.

---
name: lh-react-next
description: >
  React/Next.js feature judgment: server/client split, state placement,
  component boundaries, data flow. Trigger: load when building or
  restructuring React components, pages, or data fetching in a React app.
license: Apache-2.0
metadata:
  author: jrodrigopuca
  version: "1.1"
  local-agents-id: "sk-750795"
---

_Verified against React 19, React Compiler 1.0 and Next.js 16 (September
2026). Two facts here are version-bound — whether the compiler is on, and what
Next caches by default — so when the year or the major has changed, re-check
them before teaching._

## Critical Patterns

### 1. Server by default, client by exception

In Next.js App Router, every component is a server component until it earns
`'use client'` with a concrete reason: event handlers, browser APIs, stateful
interactivity. Push the directive to the **leaves**: `'use client'` marks a
module boundary, and everything that module IMPORTS joins the client bundle. A
server component passed INTO a client component as `children` (or any prop)
still renders on the server — the boundary follows imports, not the visual
tree. So a client provider or layout wrapper does not by itself drag the page
under it to the client; importing that page's components into it does. When
in doubt, split: server shell, small client island, server content passed in
as `children`. In a client-only app (Vite, React
Native, a pure SPA) this pattern has nothing to apply to — everything is
client, and saying so beats forcing a server/client split that buys nothing.

### 2. The state placement ladder

Place state at the LOWEST rung that satisfies the requirement, and be able to
name why it can't live one rung lower:

```
1. Derived           — computed from existing state/props: don't store it, compute it
2. Local (useState)  — one component cares
3. Lifted            — siblings share it: nearest common parent
4. URL               — survives refresh/share/back button (filters, tabs, pagination)
5. Server state      — cache of remote data: fetch layer owns it, not useState
6. Global store      — genuinely app-wide (session, theme); the LAST resort, not the first
```

Most state bugs are state living rungs above where it belongs. URL state (rung
4) is the most underused rung — reach for it before any store.

### 3. Component boundaries follow responsibility, not visuals

Split components when responsibilities diverge (fetching vs. rendering, form
logic vs. field display), not when a file "looks long". Container/presentational
still earns its keep: presentational components take data and callbacks, know
nothing about where data comes from, and are trivially testable.

### 4. Data fetching: on the server, close to use, deduped — and never assume the cache

- Fetch where the framework renders on the server: Server Components in Next
  App Router, loaders in Remix / React Router / TanStack Start (Next has no
  loaders). Client-side fetching is for data that changes after interaction,
  and then a query library owns it — never a bare `useEffect` + `useState`
  pair.
- Fetch where the data is used, not hoisted to the top and drilled down;
  per-request memoization (React's `cache()`, the framework's fetch dedup)
  makes colocated fetches cheap within one render.
- **What is cached by default is a property of the Next MAJOR and its config,
  and it has flipped:** 14 cached `fetch` by default, 15 stopped, 16 with
  Cache Components caches nothing until a function or page says
  `'use cache'`. Read the version and `next.config` before assuming anything
  is cached or fresh, and say which version your advice targets.
- Mutations go through Server Actions with revalidation, and the UI reflects
  pending and error states (`useActionState` gives you both) — no
  fire-and-forget.

### 5. Loading and failure boundaries are placed, not sprinkled

A `Suspense` boundary goes around the unit the user can wait for SEPARATELY —
the slow panel, not the whole page — so streaming shows the rest while it
loads; one boundary at the root is a spinner with extra steps. An error
boundary goes per route segment (`error.tsx`) so a failing widget doesn't
blank the app. State that must reset when its subject changes (a form for a
different record, a list for a different query) resets by `key`, not by
effects that clear it.

### 6. Composition before configuration

When a component sprouts boolean props (`isCompact`, `withHeader`, `hideFooter`),
stop: that's three components wearing a trench coat. Prefer `children` and slot
props over prop flags; prefer wrapping over forking.

### 7. Re-render hygiene is structural, not memo-spam

Manual `useMemo`/`useCallback` become mostly unnecessary only when the **React
Compiler is on** — it shipped 1.0 in October 2025 and Next.js 16 supports it
stable, but it is OFF until the project sets `reactCompiler: true` (or the
babel plugin). React 19 by itself memoizes nothing. Check the config before
deleting memoization, and before adding it. Either way, when something
re-renders too much, fix the STRUCTURE first: state living too high (ladder
rung too big), context holding fast-changing values, components defined inside
components. Memoization patches what placement should have prevented.

## Resources

- API-level patterns: a `react-19`, `nextjs-*`, or `zustand-*` skill if the host
  exposes one — all ship outside this catalog, so treat them as optional and fall
  back to each framework's current docs, stating the version you targeted
- Sibling skill: [lh-fullstack-boundaries](../lh-fullstack-boundaries/SKILL.md) — where
  the component tree meets the wire