
React Advanced
- 79 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
react-advanced is a reference skill for React 19 platform features and rendering architecture, covering the React Compiler, concurrent rendering, Actions, and the RSC boundary.
About
This skill is a reference for React 19 platform features and rendering architecture. It covers the React Compiler and auto-memoization, concurrent rendering with useTransition and Suspense, the use() hook, Actions and form hooks, advanced component patterns, virtualization, code-splitting, and the RSC/'use client' boundary. A developer uses it when adopting React 19 features or optimizing render performance.
- React 19 Compiler auto-memoization and the end of reflexive useMemo/useCallback
- Concurrent rendering (useTransition, useDeferredValue, Suspense) and the use() hook
- Actions form hooks, advanced component patterns, and the RSC/'use client' boundary
React Advanced by the numbers
- 79 all-time installs (skills.sh)
- Ranked #1,117 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
react-advanced capabilities & compatibility
- Capabilities
- react development · render optimization · concurrent rendering
- Use cases
- frontend · refactoring
- Runs
- Runs locally
- Pricing
- Free
What react-advanced says it does
The reference for React-19-era platform capabilities and rendering architecture.
Pre-19 memoization advice — wrapping values in `useMemo`/`useCallback`/`React.memo` by reflex — is now an anti-pattern for new code.
This guidance treats **React 19 (Dec 2024) and React Compiler 1.0 (stable Oct 2025)** as current.
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill react-advancedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 79 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
What it does
Reference for React 19 platform features: the React Compiler, concurrent rendering, Actions/use() hooks, and the RSC boundary.
Who is it for?
Developers adopting React 19 features or optimizing render performance in advanced apps.
Skip if: Beginners learning core React hooks and JSX (use react-core instead).
When should I use this skill?
Adopting the React Compiler, using concurrent features, building with React 19 Actions/forms or use(), or optimizing render performance.
What you get
React 19 code using the Compiler, concurrent features, and Actions correctly.
By the numbers
- Bundles 6 reference files (react-compiler, concurrent-rendering, actions, component-patterns, performance, rsc-boundary)
Files
React Advanced: React 19 Platform & Rendering Architecture
Overview
The reference for React-19-era platform capabilities and rendering architecture. This skill covers the features that reshape how modern React applications are built and optimized: the React Compiler and the end of reflexive manual memoization, concurrent rendering primitives, the use() hook, Actions and the form hooks, the React 19 ergonomic changes (ref as a plain prop, <Context> as provider, native metadata), advanced component-architecture patterns, rendering-performance engineering, and a conceptual treatment of the React Server Component boundary.
This guidance treats React 19 (Dec 2024) and React Compiler 1.0 (stable Oct 2025) as current. Pre-19 memoization advice — wrapping values in useMemo/useCallback/React.memo by reflex — is now an anti-pattern for new code. The compiler memoizes automatically. Authoritative documentation lives at react.dev; this skill distills and applies it rather than restating it.
Boundary Map: What This Skill Does NOT Cover
This skill stays focused on the React-19 platform and rendering architecture. Defer to the owning skill for everything else:
| Topic | Owning skill |
|---|---|
Core hooks tutorial (useState/useEffect/useRef/useMemo/useCallback/useContext), JSX, props/state, lists, controlled inputs | react-core |
| Advanced custom-hook composition recipes — SWR-backed data hooks, debounced search, memoized-provider value pattern, discriminated-union hook states | react-hooks-composition |
Explicit finite-state-machine modeling with XState v5 (setup(), actors, guards, parallel states) | react-state-machine |
| Server-state caching, query invalidation, optimistic updates via React Query | tanstack-query |
| Global client state via a store with no providers | zustand |
Framework wiring of Server Components, Server Actions (revalidateTag/revalidatePath), App Router data fetching, caching, Turbopack | Next.js skills (nextjs-core, nextjs-v16) |
| Docking layout UI (drag-drop panels, splitters) | flexlayout-react |
This skill explains the React-level concepts (concurrent rendering, the RSC boundary as a React feature, Actions as a React primitive). Framework-specific wiring of those concepts belongs to the Next.js skills.
When to Use This Skill
Reach for this skill when:
- Adopting the React Compiler and untangling the old memoization habits
- Using concurrent features (
useTransition,useDeferredValue, Suspense for data) to keep a UI responsive - Building with React 19 Actions, forms, or the
use()hook - Optimizing render performance: context over-rendering, large lists, code-splitting
- Architecting compound components, context selectors, error boundaries, or portals
- Reasoning about the RSC /
'use client'boundary at the React level
The React Compiler — and the End of Manual Memoization
The React Compiler reached 1.0 stable on 2025-10-07. It is a build-time tool that auto-memoizes components and hooks at fine granularity, including conditional memoization that hand-written useMemo cannot express. It works with React 17, 18, and 19, and performs best on 19.
The change vs React 17/18: the long-standing advice to wrap values in useMemo/useCallback and components in React.memo is obsolete for new code. The compiler handles memoization. Scattering manual memo by reflex now adds noise and can be net-negative.
// BEFORE (pre-compiler reflex): hand-memoize everything
const sorted = useMemo(() => items.slice().sort(compare), [items]);
const onPick = useCallback((id: string) => onSelect(id), [onSelect]);
const Row = React.memo(function Row({ item }: { item: Item }) { /* ... */ });// AFTER (compiler enabled): write plain code; the compiler memoizes
const sorted = items.slice().sort(compare);
const onPick = (id: string) => onSelect(id);
function Row({ item }: { item: Item }) { /* ... */ }When manual memo still matters (the escape hatch):
- A computed value feeds an effect dependency array and must hold a stable reference for correctness, not just performance.
- Code the compiler cannot statically analyze — it safely skips such components rather than miscompiling them, so manual memo remains a valid local optimization.
Existing code: the compiler preserves manual memoization on purpose; the preserve-manual-memoization lint flags memo that was load-bearing. Do not strip useMemo/useCallback blindly after enabling the compiler.
Adoption path: install eslint-plugin-react-hooks v6+ (it absorbed the former eslint-plugin-react-compiler) → fix the reported Rules of React violations → enable the compiler in the build (Babel / Vite / Metro / Rsbuild). React DevTools shows a "Memo ✨" badge on compiler-optimized components.
Full adoption guide, per-bundler setup, and before/after analysis: react-compiler.md.
Concurrent Rendering
Concurrent features let React interrupt and prioritize rendering so urgent updates (typing) stay responsive while expensive updates (filtering a large list) yield.
- `useTransition` marks a state update as non-urgent and exposes
isPending. React keeps the old UI interactive while the transition renders in the background. - React 19 async transitions:
startTransitionaccepts async functions. The gotcha: state updates issued after anawaitfall outside the transition unless re-wrapped instartTransition. - `useDeferredValue` produces a lagging copy of a value and is interruptible and device-adaptive. Prefer it over a fixed
setTimeoutdebounce for expensive derived renders; React 19 adds aninitialValue. - Suspense for data suspends on a thrown promise (or one read via
use()) and shows the nearest<Suspense fallback>. A changingkeyresets a boundary.
function ProductSearch({ allProducts }: { allProducts: Product[] }) {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query); // lags behind input under load
// Filtering reads the deferred value, so typing stays responsive.
const results = allProducts.filter((p) => p.name.includes(deferredQuery));
return (
<>
<input value={query} onChange={(e) => setQuery(e.target.value)} />
<ResultsList results={results} stale={query !== deferredQuery} />
</>
);
}Streaming SSR at the React level (renderToPipeableStream, shell design, Suspense reveal batching in 19.2) is covered in concurrent-rendering.md; framework streaming specifics defer to the Next.js skills.
React 19 Platform Hooks & APIs
The use() hook
use() reads a promise or a context value and may be called conditionally — after early returns — a deliberate exception to the Rules of Hooks. It cannot sit inside try/catch; surface rejected promises through an Error Boundary instead. Prefer creating the promise high in the tree (or on the server) and passing it down, since a promise created during render is recreated each render.
Actions
Actions are the React-level async-mutation primitive, independent of any framework:
- `useActionState` (renamed from the canary
useFormState) runs an action, tracking pending and error state and threading the previous result forward. - `useOptimistic` renders an optimistic value while the action is in flight, reverting automatically on completion.
- `useFormStatus` reads the pending state of the enclosing
<form>from a child, with no prop drilling. - `<form action={fn}>` wires a function directly to submission, auto-resetting on success.
function NameForm({ save }: { save: (name: string) => Promise<string> }) {
const [error, submit, isPending] = useActionState(
async (_prev: string | null, formData: FormData) => {
const result = await save(formData.get('name') as string);
return result.startsWith('error') ? result : null;
},
null,
);
return (
<form action={submit}>
<input name="name" disabled={isPending} />
{error && <p role="alert">{error}</p>}
</form>
);
}Ergonomic changes
- `ref` is a plain prop.
forwardRefis deprecated (a codemod migrates existing usage); a function component receivesreflike any other prop. - `<Context value>` replaces
<Context.Provider>, which is now legacy. - Native document metadata:
<title>,<meta>, and<link>hoist to<head>automatically, replacingreact-helmet.preload/preinit/preconnecthandle resource hints.
Complete examples and migration notes from 17/18: react19-actions-and-apis.md.
Advanced Component Patterns
- Compound components share implicit state through context so a parent and its sub-components coordinate without prop drilling (
<Tabs>/<Tabs.Tab>). - Context optimization: native context has no selector. Any change to a provider value re-renders all consumers;
React.memodoes not block context-driven re-renders, anduse(Context)adds conditional reading but not selective subscription. The compiler does not fix large-context over-rendering. The native fixes are memoizing the provider value and splitting contexts by change frequency; the userlanduse-context-selectoradds slice subscriptions (with tearing/legacy caveats). - Render props vs custom hooks: hooks win for logic reuse; render props remain useful for headless "what to render" components.
- Error boundaries are still class-based;
react-error-boundaryadds a hook (useErrorBoundary) for async and event-handler errors that native boundaries miss. React 19 logs caught errors once and addsonCaughtError/onUncaughtError. - Portals (
createPortal) change DOM placement only — context and errors still flow through the React tree, not the DOM tree.
Decision guides and full examples: component-patterns.md.
Performance Architecture
- Virtualization renders only visible rows.
@tanstack/react-virtualis the modern headless choice with the strongest TypeScript story;react-windowis mature but stalled;react-virtuosois batteries-included. After virtualizing, the per-item renderer cost becomes the bottleneck — keep rows light. - Code-splitting with
React.lazy+Suspensesplits by route first. Avoid over-splitting tiny components into many sub-10KB chunks, which adds request overhead without payoff. - Profiling uses the React DevTools Profiler against a production build to see what actually re-rendered and why; Why-Did-You-Render helps trace avoidable renders.
Library comparison, code-splitting strategy, and the full anti-pattern → detection → fix table: performance.md.
RSC / 'use client' Boundary (Conceptual)
React Server Components render ahead of time, cannot hold state or run effects, and produce serializable output. The boundary is defined on the module dependency graph (a 'use client' directive at the top of a module), not on the render tree. Props crossing into a Client Component must be serializable, and a Client Component may still render a Server Component passed to it as children. The 'use server' directive marks Server Functions whose arguments are untrusted and must be validated and authorized.
Explicit defer: caching, route loaders, and App Router data fetching belong to the Next.js skills. Pin the React version, since RSC bundler APIs are not semver-stable within 19.x. Conceptual model and the framework hand-off: rsc-boundary.md.
Anti-Patterns to Avoid
❌ Don't: Hand-memoize everything with the compiler enabled
// BAD: redundant manual memo once the compiler is on; adds noise and cost
const value = useMemo(() => ({ a, b }), [a, b]);
const onClick = useCallback(() => doThing(a), [a]);// GOOD: write plain code; let the compiler memoize
const value = { a, b };
const onClick = () => doThing(a);❌ Don't: Strip manual memo blindly after enabling the compiler
// BAD: removing memo that stabilized an effect dependency changes behavior
const config = { endpoint, token }; // recreated each render → effect re-fires
useEffect(() => subscribe(config), [config]);// GOOD: keep the deliberate escape hatch the preserve-manual-memoization lint protects
const config = useMemo(() => ({ endpoint, token }), [endpoint, token]);
useEffect(() => subscribe(config), [config]);❌ Don't: Put high-frequency state in one large context
// BAD: every consumer re-renders when any field changes; memo cannot block it
const AppContext = createContext<{ theme: Theme; cursor: Point } | null>(null);// GOOD: split by change frequency so fast-changing state is isolated
const ThemeContext = createContext<Theme | null>(null); // changes rarely
const CursorContext = createContext<Point | null>(null); // changes often❌ Don't: Update state after await inside an async transition
// BAD: the post-await update escapes the transition; isPending ends early
startTransition(async () => {
const data = await load();
setData(data); // not part of the transition
});// GOOD: re-wrap the post-await update
startTransition(async () => {
const data = await load();
startTransition(() => setData(data));
});❌ Don't: Keep forwardRef / <Context.Provider> / react-helmet in new React 19 code
// BAD: superseded idioms in new code
const Input = forwardRef<HTMLInputElement, Props>((props, ref) => <input ref={ref} {...props} />);
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>;// GOOD: ref as a plain prop, <Context> as provider, native metadata
function Input({ ref, ...props }: Props & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}
<ThemeContext value={theme}>{children}</ThemeContext>;The & { ref?: ... } intersection is only needed when Props is a custom interface that must expose ref. For wrappers over an intrinsic element, deriving Props from React.ComponentProps<'input'> already includes a correctly-typed ref, so the intersection is redundant.
When to Reach for Sibling Skills
- react-core: fundamentals — components, JSX, props/state, core hooks, lists, controlled inputs.
- react-hooks-composition: custom-hook composition recipes — SWR data hooks, debounced search, memoized providers, async-state hooks.
- react-state-machine: explicit XState v5 finite-state machines for complex flows where impossible states must be unrepresentable.
- tanstack-query / zustand: server-state caching and global client state, respectively.
- Next.js skills: framework wiring of Server Components, Server Actions, routing, and caching.
Best Practices Summary
1. Compiler first: enable eslint-plugin-react-hooks v6+, fix Rules of React, then turn on the React Compiler. 2. Stop reflexive memoization: reach for useMemo/useCallback/React.memo only as a deliberate escape hatch (effect-dependency stability, or unanalyzable code). 3. Stay responsive with concurrency: prefer useTransition/useDeferredValue over manual debounce for expensive renders; re-wrap post-await updates. 4. Use Actions for mutations: useActionState + useOptimistic + useFormStatus cover pending, error, and optimistic states at the React level. 5. Read `use()` correctly: call it conditionally, never in try/catch, and create promises high in the tree. 6. Adopt React 19 ergonomics: ref as a prop, <Context value>, native metadata — drop forwardRef and react-helmet. 7. Fix context over-rendering structurally: memoize the value and split contexts; the compiler does not solve this. 8. Engineer list and bundle performance: virtualize large lists, code-split by route, and profile against production builds. 9. Respect the RSC boundary: it is module-graph-based; keep crossing props serializable, validate server-function inputs, and pin the React version.
Navigation
- [React Compiler](references/react-compiler.md): adoption per bundler, ESLint/Rules of React, what the compiler skips,
preserve-manual-memoization, before/after examples. - [Concurrent Rendering](references/concurrent-rendering.md):
useTransition(sync/async + post-await gotcha),useDeferredValue, Suspense-for-data, streaming SSR shells, reveal batching, boundarykeyresets. - [React 19 Actions & APIs](references/react19-actions-and-apis.md):
use(),useActionState,useOptimistic,useFormStatus,<form action>, ref-as-prop,useImperativeHandle,<Context value>, metadata/preloading, with migration notes. - [Component Patterns](references/component-patterns.md): compound components, context selectors/splitting +
use-context-selector, render-props-vs-hooks, error boundaries, portals. - [Performance](references/performance.md): virtualization library comparison, code-splitting strategy, Profiler workflow, anti-pattern → detection → fix catalog.
- [RSC Boundary](references/rsc-boundary.md): conceptual
'use client'/'use server'model, serialization rules, server-function security, Next.js hand-off, version pinning.
References
- React Documentation — authoritative source for all APIs in this skill
- React Compiler 1.0
- React 19 release
- React 19.2
- Rules of Hooks
{
"name": "react-advanced",
"version": "1.0.0",
"category": "toolchain",
"toolchain": "javascript",
"framework": "react",
"tags": [
"react",
"react-19",
"react-compiler",
"concurrent",
"useTransition",
"useDeferredValue",
"suspense",
"use-hook",
"useActionState",
"useOptimistic",
"actions",
"performance",
"virtualization",
"code-splitting",
"error-boundaries",
"portals",
"context-optimization",
"rsc"
],
"entry_point_tokens": 90,
"full_tokens": 13700,
"author": "Claude MPM Team",
"license": "MIT",
"requires": [],
"related_skills": [
"react-core",
"react-hooks-composition",
"react-state-machine"
],
"updated": "2026-06-15",
"source_path": "toolchains/javascript/frameworks/react/react-advanced/SKILL.md",
"source": "https://github.com/bobmatnyc/claude-mpm",
"created": "2026-06-15",
"modified": "2026-06-15",
"maintainer": "Claude MPM Team",
"attribution_required": true,
"repository": "https://github.com/bobmatnyc/claude-mpm-skills",
"has_references": true,
"reference_files": [
"react-compiler.md",
"concurrent-rendering.md",
"react19-actions-and-apis.md",
"component-patterns.md",
"performance.md",
"rsc-boundary.md"
]
}
Advanced Component Patterns
Source of truth: useContext, createPortal, and Component (error boundaries). This reference applies that guidance and notes the userland gaps it does not cover.
Compound Components
Compound components let a parent and its sub-components coordinate through implicit shared state, so the consumer composes the pieces declaratively without wiring props between them. The shared state travels through context.
const TabsContext = createContext<{
active: string;
setActive: (id: string) => void;
} | null>(null);
function Tabs({ defaultTab, children }: { defaultTab: string; children: React.ReactNode }) {
const [active, setActive] = useState(defaultTab);
return <TabsContext value={{ active, setActive }}>{children}</TabsContext>;
}
function Tab({ id, children }: { id: string; children: React.ReactNode }) {
const ctx = useTabs();
return (
<button aria-selected={ctx.active === id} onClick={() => ctx.setActive(id)}>
{children}
</button>
);
}
function TabPanel({ id, children }: { id: string; children: React.ReactNode }) {
const ctx = useTabs();
return ctx.active === id ? <div role="tabpanel">{children}</div> : null;
}
function useTabs() {
const ctx = useContext(TabsContext);
if (!ctx) throw new Error('Tabs.* must render inside <Tabs>');
return ctx;
}
// Usage: the parent owns state; sub-components read it implicitly.
<Tabs defaultTab="a">
<Tab id="a">First</Tab>
<Tab id="b">Second</Tab>
<TabPanel id="a">First panel</TabPanel>
<TabPanel id="b">Second panel</TabPanel>
</Tabs>;Context Optimization
Native context has no selector. When a provider value changes, every consumer of that context re-renders, regardless of which slice each consumer reads. Three facts shape every fix:
React.memodoes not block context-driven re-renders. A memoized consumer still re-renders when its context value changes.use(Context)adds conditional reading but not selective subscription — a consumer still subscribes to the whole value.- The React Compiler does not solve large-context over-rendering. This is an architectural problem, not a memoization one.
Native fix 1 — memoize the provider value
A provider value object created inline is a new reference every render, re-rendering all consumers even when nothing changed. Memoize it. (The full memoized-provider recipe lives in react-hooks-composition; the point here is that it is necessary but not sufficient.)
const value = useMemo(() => ({ user, signOut }), [user, signOut]);
return <AuthContext value={value}>{children}</AuthContext>;Native fix 2 — split contexts by change frequency
Separate slow-changing state from fast-changing state so a frequent update does not re-render consumers that only care about stable data.
// BAD: theme (rare) and pointer (frequent) share one context.
const UIContext = createContext<{ theme: Theme; pointer: Point } | null>(null);// GOOD: independent contexts; pointer churn never re-renders theme consumers.
const ThemeContext = createContext<Theme | null>(null);
const PointerContext = createContext<Point | null>(null);Userland fix — use-context-selector
When a single context must hold many slices and consumers need to subscribe to one slice, the userland use-context-selector library provides selective subscription that native context lacks.
import { createContext, useContextSelector } from 'use-context-selector';
const StoreContext = createContext<Store | null>(null);
// Re-renders only when `store.count` changes, not on every store update.
const count = useContextSelector(StoreContext, (s) => s!.count);Caveats: it predates the React 19 idioms (uses its own provider), and selective subscription interacts with concurrent rendering's tearing guarantees — evaluate against a dedicated state library (zustand for global client state) when slice subscription becomes the dominant need.
Render Props vs Custom Hooks
For reusing logic, custom hooks win: they compose, avoid the "wrapper hell" of nested render-prop components, and read linearly. Render props remain useful for headless components that own behavior but delegate what to render to the consumer.
// Logic reuse → hook
function useHover() {
const [hovered, setHovered] = useState(false);
const bind = { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false) };
return [hovered, bind] as const;
}// Headless "what to render" → render prop
function Toggle({ children }: { children: (on: boolean, toggle: () => void) => React.ReactNode }) {
const [on, setOn] = useState(false);
return <>{children(on, () => setOn((v) => !v))}</>;
}Error Boundaries
Error boundaries are still class-based — there is no function-component equivalent. They catch errors thrown during render, in lifecycle methods, and in constructors of the tree below them. They do not catch errors in event handlers, async code, or the boundary's own render.
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() { return { hasError: true }; }
componentDidCatch(error: Error, info: React.ErrorInfo) { report(error, info); }
render() { return this.state.hasError ? this.props.fallback : this.props.children; }
}For async and event-handler errors, react-error-boundary adds useErrorBoundary().showBoundary(error) to forward a caught error into the nearest boundary. React 19 logs caught errors once (no duplicate console noise) and adds root-level onCaughtError/onUncaughtError callbacks for centralized reporting.
import { useErrorBoundary } from 'react-error-boundary';
function Saver() {
const { showBoundary } = useErrorBoundary();
async function save() {
try { await api.save(); }
catch (err) { showBoundary(err); } // routes an async error to the boundary
}
return <button onClick={save}>Save</button>;
}Portals
createPortal renders children into a different DOM node while keeping them in the same place in the React tree. Context still flows, events still bubble through the React hierarchy, and errors still propagate to React-tree ancestors — only the DOM placement changes. This makes portals the right tool for modals, tooltips, and toasts that must escape an overflow/z-index container.
function Modal({ children }: { children: React.ReactNode }) {
// Rendered into document.body, but still a React child of whatever rendered <Modal>.
return createPortal(
<div className="modal-overlay">{children}</div>,
document.body,
);
}Anti-Patterns
| Anti-pattern | Why it is wrong | Detection |
|---|---|---|
| One large context for high-frequency state | Every consumer re-renders on any change; memo cannot block it | Profiler shows unrelated consumers re-rendering |
Expecting React.memo to stop context re-renders | Context bypasses prop comparison | Memoized child still re-renders on context change |
| Function-component "error boundary" | No FC equivalent exists | Render errors uncaught; only class/react-error-boundary works |
| Expecting native boundaries to catch async/event errors | They catch only render-phase errors | Event-handler throws crash the app |
| Reaching for a portal to fix layout flow | Portals change DOM placement, not React tree semantics | Context/event expectations broken by misuse |
Sources
- https://react.dev/reference/react/useContext
- https://react.dev/reference/react-dom/createPortal
- https://react.dev/reference/react/Component
- https://www.npmjs.com/package/use-context-selector
- https://github.com/dai-shi/use-context-selector
- https://newsletter.daishikato.com/p/the-past-and-future-of-render-optimization-with-react-context
- https://blog.logrocket.com/react-error-handling-react-error-boundary/
Concurrent Rendering
Source of truth: useTransition, useDeferredValue, Suspense, and renderToPipeableStream. This reference applies that guidance.
Concurrent rendering lets React prepare a new UI in the background and interrupt it for more urgent work. Urgent updates (typing, clicking) stay responsive while expensive updates (filtering thousands of rows, navigating) render without blocking input.
useTransition
useTransition marks a state update as a non-urgent transition. React renders the transition in the background, keeps the current UI interactive, and exposes isPending so the UI can show a subtle pending indication without a hard fallback.
function TabbedView() {
const [tab, setTab] = useState<'home' | 'reports'>('home');
const [isPending, startTransition] = useTransition();
function select(next: 'home' | 'reports') {
// The tab button responds instantly; the heavy panel renders as a transition.
startTransition(() => setTab(next));
}
return (
<>
<TabBar active={tab} onSelect={select} busy={isPending} />
{tab === 'reports' ? <ExpensiveReports /> : <Home />}
</>
);
}React 19 async transitions and the post-await gotcha
React 19 allows startTransition to receive an async function (an "Action"). The subtle rule: only updates issued synchronously within the transition scope belong to the transition. Updates after an await run in a fresh task and escape it unless re-wrapped.
// BAD: setResult runs after await, outside the transition; isPending ends early.
startTransition(async () => {
const data = await fetchData();
setResult(data);
});// GOOD: re-wrap the post-await update so it stays part of the transition.
startTransition(async () => {
const data = await fetchData();
startTransition(() => setResult(data));
});startTransition (standalone) vs useDeferredValue
startTransition (the standalone import) marks an update urgent-or-not at the call site — appropriate when an event handler triggers the expensive update. useDeferredValue marks a value as allowed to lag — appropriate when an expensive subtree should trail a fast-changing input.
Prefer useDeferredValue over a fixed setTimeout debounce for expensive derived renders: it is interruptible and device-adaptive (it defers more on slow devices, less on fast ones) rather than imposing one fixed delay on every user.
function FilterList({ rows }: { rows: Row[] }) {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text, ''); // React 19 initialValue
const filtered = rows.filter((r) => r.label.includes(deferredText));
const isStale = text !== deferredText;
return (
<>
<input value={text} onChange={(e) => setText(e.target.value)} />
<ul style={{ opacity: isStale ? 0.6 : 1 }}>
{filtered.map((r) => <li key={r.id}>{r.label}</li>)}
</ul>
</>
);
}Suspense for Data
A component suspends when it reads a not-yet-resolved promise — either thrown by a data layer or read via use(). The nearest <Suspense fallback> shows until the promise resolves.
function Page({ userPromise }: { userPromise: Promise<User> }) {
return (
<Suspense fallback={<ProfileSkeleton />}>
<Profile userPromise={userPromise} />
</Suspense>
);
}
function Profile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // suspends until resolved
return <h1>{user.name}</h1>;
}Resetting a boundary with key
Changing the key on a Suspense boundary (or a component inside it) discards the previous tree and re-suspends — useful when switching the resource a boundary displays.
// A new id remounts ProfileLoader and re-suspends on the new request.
<Suspense fallback={<Skeleton />}>
<ProfileLoader key={userId} userId={userId} />
</Suspense>Fallback timing
A transition does not show a Suspense fallback for already-revealed content — that is what keeps navigation from flashing skeletons. Initial reveals still show the fallback. Design fallbacks to match the final layout to avoid layout shift.
Streaming SSR (React Level)
For Node servers, renderToPipeableStream streams HTML as Suspense boundaries resolve. Design a meaningful shell — header, navigation, layout — that flushes immediately, and let slower data stream in behind Suspense rather than blocking the whole document on one root spinner.
import { renderToPipeableStream } from 'react-dom/server';
function handler(req, res) {
const { pipe, abort } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
// The shell (everything outside Suspense) is ready: start streaming.
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html');
pipe(res);
},
onShellError(error) {
res.statusCode = 500;
res.send('<!doctype html><p>Loading failed</p>');
},
});
setTimeout(abort, 10_000); // bound the stream
}React 19.2 (2025-10-01) batches Suspense reveals that land close together — with a heuristic that protects Largest Contentful Paint — and adds Web Streams SSR plus resume for prerender-then-resume flows. renderToReadableStream serves Web-Streams runtimes (edge, Deno, Bun).
Framework defer: App Router streaming, route-level Suspense placement, and data-fetching orchestration belong to the Next.js skills. This reference covers the React-level primitives those frameworks build on.
Anti-Patterns
| Anti-pattern | Why it is wrong | Detection |
|---|---|---|
State update after await in an async transition | Escapes the transition; isPending ends early | UI flips to non-pending mid-operation |
| Fixed debounce/throttle for expensive derived renders | useDeferredValue is interruptible and adaptive | Janky typing on slow devices; hard-coded timers |
| One root spinner instead of a streamed shell | Blocks first paint on the slowest data | Long blank time-to-first-byte; no progressive reveal |
| Suspense fallback that mismatches final layout | Causes layout shift on reveal | Visible jump when content arrives |
Sources
- https://react.dev/reference/react/useTransition
- https://react.dev/reference/react/startTransition
- https://react.dev/reference/react/useDeferredValue
- https://react.dev/reference/react/Suspense
- https://react.dev/reference/react-dom/server/renderToPipeableStream
- https://react.dev/reference/react-dom/server/renderToReadableStream
- https://react.dev/blog/2025/10/01/react-19-2
Performance Architecture
Source of truth for the React APIs: lazy and Suspense. Library comparisons reflect the 2026 landscape; verify versions against each library's current docs.
Rendering-performance work splits into three concerns: rendering fewer DOM nodes (virtualization), shipping less JavaScript up front (code-splitting), and measuring before optimizing (profiling). With the React Compiler handling memoization, performance effort shifts away from hand-tuned useMemo toward these architectural levers.
Virtualization
A list of thousands of rows mounts thousands of DOM nodes, which tanks initial render and scroll performance. Virtualization renders only the rows in (and near) the viewport, recycling them as the user scrolls.
Library comparison (2026)
| Library | Character | Use when |
|---|---|---|
@tanstack/react-virtual | Modern, headless, strongest TypeScript; full control over markup | New code, custom row layouts, TS-first projects |
react-window | Mature, small, but largely stalled | Simple fixed/variable lists where a settled API matters |
react-virtuoso | Batteries-included (auto-sizing, grouping, sticky headers) | Rich list features without building them by hand |
import { useVirtualizer } from '@tanstack/react-virtual';
function BigList({ rows }: { rows: Row[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 40,
overscan: 8,
});
return (
<div ref={parentRef} style={{ height: 600, overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize(), position: 'relative' }}>
{virtualizer.getVirtualItems().map((vi) => (
<div
key={vi.key}
style={{ position: 'absolute', top: 0, transform: `translateY(${vi.start}px)`, width: '100%' }}
>
<RowView row={rows[vi.index]} />
</div>
))}
</div>
</div>
);
}After virtualizing, the bottleneck moves to the per-item renderer. Once only ~20 rows render at a time, an expensive RowView dominates. Keep row components light: avoid heavy per-row computation, deep trees, and large inline objects.
Code-Splitting
React.lazy + Suspense defers loading a component's code until it renders, shrinking the initial bundle.
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<RouteSkeleton />}>
<Settings />
</Suspense>
);
}Strategy
- Split by route first. Route boundaries are the highest-value split points: a user loads only the route they visit.
- Do not over-split. Lazy-loading many tiny (<10KB) components creates a waterfall of small requests whose overhead outweighs the savings. Reserve
lazyfor genuinely heavy or rarely visited subtrees (editors, charts, admin panels). - Place the fallback to match layout so the lazy boundary does not cause a layout shift on load.
Profiling Workflow
Measure before optimizing. The React DevTools Profiler records commits and shows which components re-rendered and why.
1. Profile a production build. Development builds carry extra work and mislead timing. Use a production (or profiling) build for representative numbers. 2. Record an interaction, then read the flamegraph: wide bars are expensive commits; the "why did this render" panel attributes the cause (props, state, hooks, parent). 3. Confirm the React Compiler is active — components show a "Memo ✨" badge. Their absence on a hot path explains avoidable re-renders. 4. Use Why-Did-You-Render in development to log re-renders caused by unstable props when chasing a specific regression.
Anti-Pattern → Detection → Fix Catalog
| Anti-pattern | Why it is wrong (2026) | How to detect | Fix |
|---|---|---|---|
| Hand-memoizing everything | Redundant with the compiler; adds noise/cost | Compiler on, code littered with manual memo | Remove; reserve memo for escape hatches |
| Removing manual memo blindly after enabling the compiler | Compiler preserves load-bearing memo (effect deps) | preserve-manual-memoization warnings; effects re-firing | Restore the meaningful memo |
| Large/high-frequency context without split or selector | Every consumer re-renders; memo cannot block it | Profiler highlights unrelated consumers | Split contexts / use-context-selector |
use() treated as a normal hook, or inside try/catch | Intentional Rules-of-Hooks exception; catch misses suspension | Errors swallowed; promises recreated each render | Error Boundary; create promise high in the tree |
State update after await in an async transition | Falls outside the transition; isPending ends early | UI flips to non-pending mid-operation | Re-wrap in startTransition |
| Manual debounce/throttle for expensive derived renders | useDeferredValue is interruptible and adaptive | Janky typing on slow devices; fixed timers | Use useDeferredValue |
| Large lists without virtualization | Thousands of DOM nodes; long commits | High node count; long commit times in Profiler | Virtualize |
Over-splitting with React.lazy | Request overhead without payoff | Many <10KB lazy chunks; request waterfall | Split by route; coarser chunks |
Fetch waterfalls (sequential await, fetch-in-useEffect) | Serial latency; client round-trips | Sequential network spans; chained data hooks | Parallelize; hoist data fetching |
Legacy forwardRef / <Context.Provider> / react-helmet in new code | Superseded by React 19 idioms | Deprecation lint; legacy idioms in new files | Ref-as-prop, <Context value>, native metadata |
Sources
- https://react.dev/reference/react/lazy
- https://react.dev/reference/react/Suspense
- https://www.pkgpulse.com/guides/tanstack-virtual-vs-react-window-vs-react-virtuoso-2026
- https://stevekinney.com/courses/react-performance/code-splitting-and-lazy-loading
- https://www.growin.com/blog/react-performance-optimization-2025/
- https://blog.openreplay.com/scan-react-code-anti-patterns-react-doctor/
React Compiler — Adoption & Memoization Guidance
Source of truth: React Compiler docs and the 1.0 announcement. This reference applies that guidance; consult react.dev for the canonical API.
What the Compiler Is
The React Compiler is a build-time optimizing compiler that rewrites components and hooks to memoize automatically. It reached 1.0 stable on 2025-10-07. It analyzes which values a component computes, which of those depend on which inputs, and emits memoization at fine granularity — including conditional memoization that a hand-written useMemo cannot express, because a manual useMemo runs unconditionally with a fixed dependency array.
It supports React 17, 18, and 19, and produces the best results on 19. On 17/18 it relies on a runtime helper (react-compiler-runtime).
The Memoization Inversion
Pre-compiler React guidance optimized for reference stability by hand: wrap derived values in useMemo, wrap callbacks in useCallback, wrap components in React.memo. With the compiler enabled, that work is redundant for new code, and over-memoization carries real cost (larger code, more cache slots, harder reading).
Before (manual, pre-compiler)
function Dashboard({ items, onSelect }: Props) {
const sorted = useMemo(() => items.slice().sort(byName), [items]);
const total = useMemo(() => items.reduce((s, i) => s + i.value, 0), [items]);
const handleSelect = useCallback((id: string) => onSelect(id), [onSelect]);
return <List items={sorted} total={total} onSelect={handleSelect} />;
}
const List = React.memo(function List({ items, total, onSelect }: ListProps) {
return /* ... */;
});After (compiler enabled)
// Plain code. The compiler memoizes `sorted`, `total`, `handleSelect`,
// and the List render output as needed — including conditionally.
function Dashboard({ items, onSelect }: Props) {
const sorted = items.slice().sort(byName);
const total = items.reduce((s, i) => s + i.value, 0);
const handleSelect = (id: string) => onSelect(id);
return <List items={sorted} total={total} onSelect={handleSelect} />;
}
function List({ items, total, onSelect }: ListProps) {
return /* ... */;
}When Manual Memo Still Earns Its Place
The compiler does not eliminate every reason to memoize by hand. Keep manual memo when:
1. A value is an effect dependency and correctness depends on its reference identity. The compiler optimizes render; it does not guarantee a particular identity contract for an effect's dependency array. A value recreated each render re-fires the effect.
// Correctness, not just performance: a stable `config` prevents the
// subscription effect from tearing down and re-subscribing every render.
const config = useMemo(() => ({ url, token }), [url, token]);
useEffect(() => subscribe(config), [config]);2. Code the compiler cannot statically analyze. The compiler is conservative: when it cannot prove a component follows the Rules of React, it skips that component entirely (leaving it un-optimized) rather than risk a miscompile. In a skipped component, manual memo remains a valid local optimization.
Do Not Strip Existing Manual Memo Blindly
The compiler is designed to preserve existing useMemo/useCallback/React.memo. Some of that memo was load-bearing (effect-dependency stability, third-party identity contracts). The `preserve-manual-memoization` lint flags cases where removing or altering manual memo would change behavior. Treat its warnings as a signal that the memo carried meaning beyond performance.
Adoption Path
Order matters — lint and fix before enabling the compiler:
1. Install ESLint support. eslint-plugin-react-hooks v6+ absorbed the former eslint-plugin-react-compiler. Enable the recommended config.
npm install --save-dev eslint-plugin-react-hooks@^62. Fix the reported Rules of React violations. The plugin surfaces mutations during render, conditional hook calls, and other patterns that prevent safe compilation.
3. Enable the compiler in the build. Add the compiler to the toolchain — Babel, Vite, Metro, or Rsbuild.
// vite.config.ts
import react from '@vitejs/plugin-react';
export default {
plugins: [
react({
babel: { plugins: [['babel-plugin-react-compiler', {}]] },
}),
],
};4. Verify in DevTools. React DevTools marks compiler-optimized components with a "Memo ✨" badge.
Rollout Strategy for Existing Codebases
- Start in a directory-scoped mode if the build plugin supports it, expanding as the lint passes clean.
- Do not mix a half-migrated mental model: once the compiler is on for a module, write new code plainly and reserve manual memo for the documented escape hatches.
- Keep
preserve-manual-memoizationenabled so refactors do not silently remove meaningful memo.
Detection Checklist
| Symptom | Likely cause | Action |
|---|---|---|
New code still littered with useMemo/useCallback | Reflex memoization habit | Remove; let the compiler handle it |
preserve-manual-memoization warnings after a cleanup | Removed load-bearing memo | Restore the memo; it was an effect/identity contract |
| A component shows no "Memo ✨" badge | Compiler skipped it (unanalyzable) | Fix Rules of React violations, or accept manual memo there |
| Effect re-fires every render | A dependency value is recreated each render | Memoize that single dependency by hand |
Sources
- https://react.dev/blog/2025/10/07/react-compiler-1
- https://react.dev/learn/react-compiler/introduction
- https://react.dev/learn/react-compiler/installation
- https://react.dev/reference/eslint-plugin-react-hooks
- https://react.dev/reference/eslint-plugin-react-hooks/lints/preserve-manual-memoization
React 19 Platform Hooks & APIs
Source of truth: use(), useActionState, useOptimistic, useFormStatus, <form>, and the React 19 release notes. This reference applies that guidance.
The use() Hook
use() reads the value of a promise or a context. Unlike every other hook, it may be called conditionally — after early returns, inside branches — a deliberate exception to the Rules of Hooks. When given a promise, it suspends the component until the promise resolves, integrating with the nearest Suspense boundary.
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
// Legal after an early return — `use` is exempt from the top-level rule.
const comments = use(commentsPromise);
return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}
function ThemedButton() {
const theme = use(ThemeContext); // also reads context, conditionally if needed
return <button className={theme}>Save</button>;
}Rules and pitfalls
- Never wrap `use()` in `try/catch`. A suspended promise is not a thrown synchronous error; rejections surface through an Error Boundary, not
catch. - Do not create the promise during render on the client. A promise created in the render body is a new promise every render, which defeats caching and can loop. Create it high in the tree, in an event handler, or on the server, and pass it down.
// BAD: new promise each render → re-suspends endlessly on the client.
function Profile({ id }: { id: string }) {
const user = use(fetchUser(id));
return <h1>{user.name}</h1>;
}// GOOD: the promise is created once by a parent/server and passed down.
function Profile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise);
return <h1>{user.name}</h1>;
}Actions
Actions are React's built-in async-mutation model. They provide pending, error, and optimistic handling at the React level, independent of any framework.
useActionState
Runs an action function, tracks pending state, and threads the previous result forward. It replaces the canary useFormState (same idea, renamed and moved to react).
function UpdateName({ save }: { save: (name: string) => Promise<{ error?: string }> }) {
const [state, formAction, isPending] = useActionState(
async (_prev: { error?: string }, formData: FormData) => {
const res = await save(formData.get('name') as string);
return res.error ? { error: res.error } : {};
},
{},
);
return (
<form action={formAction}>
<input name="name" disabled={isPending} />
<button disabled={isPending}>Save</button>
{state.error && <p role="alert">{state.error}</p>}
</form>
);
}useOptimistic
Renders an optimistic value while an action is in flight, reverting automatically when the action settles.
function Likes({ count, like }: { count: number; like: () => Promise<void> }) {
// The update fn receives the current value plus the argument you pass to
// `addOptimistic`, so the delta is explicit rather than hard-coded.
const [optimisticCount, addOptimistic] = useOptimistic(
count,
(current, delta: number) => current + delta,
);
async function onLike() {
addOptimistic(1); // apply +1 immediately
await like(); // reverts to real `count` if this throws
}
return <button onClick={onLike}>♥ {optimisticCount}</button>;
}useFormStatus
Reads the pending state of the enclosing <form> from a child component, with no props threaded through.
function SubmitButton() {
const { pending } = useFormStatus(); // reads the parent <form>, not its own state
return <button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>;
}<form action={fn}>
A function passed to a form's action runs on submit, receiving the FormData. React resets an uncontrolled form automatically on success (use requestFormReset to opt out). Individual buttons may override with formAction.
React 19 Ergonomic Changes
ref as a plain prop
forwardRef is deprecated. A function component receives ref like any other prop. A codemod migrates existing forwardRef usage.
// BEFORE (React 18)
const TextInput = forwardRef<HTMLInputElement, Props>((props, ref) => (
<input ref={ref} {...props} />
));// AFTER (React 19): ref is just a prop
function TextInput({ ref, ...props }: Props & { ref?: React.Ref<HTMLInputElement> }) {
return <input ref={ref} {...props} />;
}When you need the `& { ref?: ... }` intersection: only when defining a custom
prop interface that must expose ref. For wrappers over an intrinsic HTML element,React 19's types already includeref— e.g. derivingPropsfrom
React.ComponentProps<'input'> (or spreading the element's HTML attributes) gives youa correctly-typed ref prop for free, so the explicit intersection is redundant there.useImperativeHandle still customizes the exposed handle; it now reads the ref prop directly.
<Context value> as provider
<Context.Provider> is legacy. Render the context itself with a value.
// BEFORE
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>// AFTER
<ThemeContext value={theme}>{children}</ThemeContext>Native document metadata
<title>, <meta>, and <link> rendered anywhere in the tree hoist to <head> automatically — react-helmet is no longer needed.
function ArticlePage({ article }: { article: Article }) {
return (
<article>
<title>{article.title}</title>
<meta name="description" content={article.summary} />
<h1>{article.title}</h1>
</article>
);
}Resource hints — preload, preinit, preconnect, prefetchDNS from react-dom — let a component declare what the browser should fetch early.
Migration Notes from 17/18
| 17/18 idiom | React 19 replacement |
|---|---|
forwardRef((props, ref) => …) | ref as a prop; codemod available |
<Context.Provider value> | <Context value> |
react-helmet for <head> tags | Native metadata hoisting |
useFormState (canary) | useActionState (from react) |
| Manual pending/error state around fetch-in-event | useActionState + useOptimistic |
Fetch in useEffect, store in state | Pass a promise to use() under Suspense |
Sources
- https://react.dev/reference/react/use
- https://react.dev/reference/react/useActionState
- https://react.dev/reference/react/useOptimistic
- https://react.dev/reference/react-dom/hooks/useFormStatus
- https://react.dev/reference/react-dom/components/form
- https://react.dev/reference/react/forwardRef
- https://react.dev/reference/react/useImperativeHandle
- https://react.dev/reference/react/createContext
- https://react.dev/reference/react-dom/components/meta
- https://react.dev/blog/2024/12/05/react-19
RSC / 'use client' Boundary (Conceptual)
Source of truth: Server Components, `'use client'`, and `'use server'`. This reference covers the React-level model; framework wiring defers to the Next.js skills.
This reference explains React Server Components as a React feature — the mental model, the boundary semantics, and the security rules. It deliberately stops at the framework edge: route loaders, caching, and App Router data fetching belong to the Next.js skills.
Server Components by Default
In an RSC setup, components render on the server ahead of time and produce a serializable description of the UI. Server Components:
- Cannot hold state (
useState), run effects (useEffect), or use browser-only APIs — they never run on the client. - Can be
asyncandawaitdata directly in the component body. - Reduce client JavaScript: their code is not shipped to the browser.
// A Server Component: async, no hooks, code stays on the server.
async function ArticlePage({ id }: { id: string }) {
const article = await db.articles.find(id); // direct data access on the server
return (
<article>
<h1>{article.title}</h1>
<LikeButton articleId={id} /> {/* a Client Component island */}
</article>
);
}The Boundary Is on the Module Graph, Not the Render Tree
The single most important model: 'use client' marks a module (and everything it imports) as client code. The boundary is drawn on the module dependency graph, not on the render tree. Once a module is 'use client', the components it defines are Client Components, and modules it imports join the client bundle.
'use client'; // this directive makes the whole module client code
import { useState } from 'react';
export function LikeButton({ articleId }: { articleId: string }) {
const [liked, setLiked] = useState(false); // hooks allowed: this is a Client Component
return <button onClick={() => setLiked(true)}>{liked ? '♥' : '♡'}</button>;
}Two consequences that surprise newcomers
1. Props crossing into a Client Component must be serializable. Numbers, strings, plain objects, arrays, and Server Functions cross the boundary; class instances, functions (other than Server Functions), and symbols do not.
2. A Client Component can render a Server Component passed as `children`. Composition through children lets a server-rendered subtree sit inside a client component without that subtree becoming client code.
// Client Component accepts server-rendered children — the children stay on the server.
'use client';
export function Collapsible({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen((v) => !v)}>Toggle</button>
{open && children} {/* may be a Server Component subtree */}
</>
);
}'use server' — Server Functions
'use server' marks a function (or a module of functions) as a Server Function: callable from the client but executed on the server. Server Functions are the mutation counterpart to Server Components.
'use server';
export async function publishComment(formData: FormData) {
const text = formData.get('text');
// Arguments arrive from an untrusted client — validate and authorize.
if (typeof text !== 'string' || text.length === 0) throw new Error('invalid');
await requireAuthenticatedUser();
await db.comments.insert({ text });
}Security rule: a Server Function's arguments are an untrusted, public-facing input boundary, exactly like an HTTP endpoint. Validate every argument and authorize the caller inside the function — never assume the client sent well-formed or permitted data.
Explicit Defer to Next.js Skills
The following are framework concerns and belong to the Next.js skills, not this skill:
- Route-level data fetching, loaders, and the App Router model.
- Caching directives (
"use cache",revalidateTag,revalidatePath) and request-time vs build-time rendering. - Bundler configuration that wires the RSC graph.
Version Pinning
RSC depends on bundler-facing APIs that are not semver-stable within React 19.x. Pin the exact React and React-DOM versions (and align them with the framework's expected versions) rather than using a caret range, so a patch bump does not break the server/client wiring.
{
"dependencies": {
"react": "19.2.0",
"react-dom": "19.2.0"
}
}Anti-Patterns
| Anti-pattern | Why it is wrong | Detection |
|---|---|---|
| Using hooks/state in a Server Component | Server Components never run on the client | Build/runtime error referencing useState in a server module |
| Passing non-serializable props across the boundary | Only serializable values and Server Functions cross | "cannot be passed to Client Components" error |
| Trusting Server Function arguments | They are a public input boundary | Missing validation/authorization in 'use server' functions |
| Caret-ranged React in an RSC app | RSC bundler APIs are not semver-stable in 19.x | Patch bump breaks server/client wiring |
| Reaching here for caching/routing | Those are framework concerns | Looking for revalidateTag in this skill — use Next.js skills |
Sources
- https://react.dev/reference/rsc/server-components
- https://react.dev/reference/rsc/use-client
- https://react.dev/reference/rsc/use-server
Related skills
FAQ
Does the React Compiler replace manual memoization?
Yes, it auto-memoizes, so useMemo/useCallback become escape hatches rather than reflexive wraps.
What React version does this skill target?
React 19 (Dec 2024) and React Compiler 1.0 (stable Oct 2025).