@3lineas/grooveskills 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ # Advanced Component Patterns
2
+
3
+ > Source of truth: [useContext](https://react.dev/reference/react/useContext), [createPortal](https://react.dev/reference/react-dom/createPortal), and [Component (error boundaries)](https://react.dev/reference/react/Component).
4
+
5
+ ## Compound Components
6
+
7
+ Compound components let a parent and its sub-components coordinate through implicit shared state via Context so consumers compose pieces declaratively without prop drilling. In GrooveSkills projects, keep each sub-component under 200 lines or extract sub-components into dedicated files under the same domain folder.
8
+
9
+ ```tsx
10
+ import { createContext, useContext, useMemo, useState, type ReactNode } from "react";
11
+
12
+ interface TabsContextValue {
13
+ activeTab: string;
14
+ setActiveTab: (id: string) => void;
15
+ }
16
+
17
+ const TabsContext = createContext<TabsContextValue | null>(null);
18
+
19
+ export function useTabsContext(): TabsContextValue {
20
+ const ctx = useContext(TabsContext);
21
+ if (!ctx) {
22
+ throw new Error("Tab components must be rendered inside <TabsRoot>.");
23
+ }
24
+ return ctx;
25
+ }
26
+
27
+ export function TabsRoot({
28
+ defaultTab,
29
+ children,
30
+ }: {
31
+ defaultTab: string;
32
+ children: ReactNode;
33
+ }) {
34
+ const [activeTab, setActiveTab] = useState<string>(defaultTab);
35
+ const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]);
36
+
37
+ return <TabsContext value={value}>{children}</TabsContext>;
38
+ }
39
+ ```
40
+
41
+ ## Context Optimization
42
+
43
+ Native context has **no selector**. When a provider value changes, **every** consumer of that context re-renders, regardless of which slice each consumer reads:
44
+
45
+ - `React.memo` does **not** block context-driven re-renders.
46
+ - `use(Context)` adds *conditional reading* but **not** *selective subscription*.
47
+ - The React Compiler does **not** solve large-context over-rendering.
48
+
49
+ ### Native fix 1 — Memoize the provider value
50
+
51
+ A provider value object created inline is a new reference every render. Memoize it when passing an object literal:
52
+
53
+ ```tsx
54
+ const authValue = useMemo(() => ({ user, signOut }), [user, signOut]);
55
+ return <AuthContext value={authValue}>{children}</AuthContext>;
56
+ ```
57
+
58
+ ### Native fix 2 — Split contexts by change frequency
59
+
60
+ Separate slow-changing state from fast-changing state so frequent updates never re-render consumers of stable data:
61
+
62
+ ```tsx
63
+ // ❌ BAD: theme (rare) and pointer (frequent) share one context.
64
+ const UIContext = createContext<{ theme: ThemeMode; pointer: Point } | null>(null);
65
+
66
+ // ✅ GOOD: independent contexts; pointer churn never re-renders theme consumers.
67
+ export const ThemeContext = createContext<ThemeMode>("light");
68
+ export const PointerContext = createContext<Point>({ x: 0, y: 0 });
69
+ ```
70
+
71
+ ### Userland fix — `use-context-selector` or External Stores
72
+
73
+ When a single store holds many slices and consumers must subscribe to a single slice, use `use-context-selector` or a dedicated external store (`useSyncExternalStore` / Zustand / Nanostores).
74
+
75
+ ## Custom Hooks vs Render Props
76
+
77
+ - **Custom Hooks (`use-*.ts`)**: Default choice for sharing stateful logic cleanly across components.
78
+ - **Render Props**: Useful for headless UI wrappers that own behavior/DOM measurement and delegate markup rendering to the caller.
79
+
80
+ ## Error Boundaries
81
+
82
+ Error boundaries catch errors thrown during render, lifecycle methods, and constructors of the tree below them. For async and event-handler errors, pair `react-error-boundary` (`useErrorBoundary().showBoundary(error)`) with React 19's root `onCaughtError` / `onUncaughtError` hooks.
83
+
84
+ ```tsx
85
+ import { useErrorBoundary } from "react-error-boundary";
86
+ import { saveRecordAction } from "@/actions/records/save-record.action";
87
+
88
+ export function SaveRecordButton({ payload }: { payload: string }) {
89
+ const { showBoundary } = useErrorBoundary();
90
+
91
+ async function handleSave(): Promise<void> {
92
+ try {
93
+ await saveRecordAction(payload);
94
+ } catch (error) {
95
+ showBoundary(error);
96
+ }
97
+ }
98
+
99
+ return (
100
+ <button type="button" onClick={handleSave}>
101
+ Save Changes
102
+ </button>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ## Portals
108
+
109
+ `createPortal` renders children into a different DOM node (`document.body`) while keeping them in the same place in the **React tree** (preserving Context and React event bubbling). Use portals for modals, dialogs, tooltips, and toasts that must escape parent `overflow: hidden` or `z-index` stacking contexts.
110
+
111
+ ## Anti-Patterns
112
+
113
+ | Anti-pattern | Why it is wrong | Detection |
114
+ |---|---|---|
115
+ | One large context for high-frequency state | Every consumer re-renders on any change; `memo` cannot block it | Profiler shows unrelated consumers re-rendering |
116
+ | Expecting `React.memo` to stop context re-renders | Context bypasses prop comparison | Memoized child still re-renders on context change |
117
+ | Expecting native boundaries to catch async/event errors | Native boundaries catch only render-phase errors | Unhandled promise rejections in click handlers |
@@ -0,0 +1,130 @@
1
+ # Concurrent Rendering
2
+
3
+ > Source of truth: [useTransition](https://react.dev/reference/react/useTransition), [useDeferredValue](https://react.dev/reference/react/useDeferredValue), [Suspense](https://react.dev/reference/react/Suspense), and [renderToPipeableStream](https://react.dev/reference/react-dom/server/renderToPipeableStream).
4
+
5
+ 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.
6
+
7
+ ## `useTransition`
8
+
9
+ `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 indicator without a hard fallback.
10
+
11
+ ```tsx
12
+ import { useState, useTransition } from "react";
13
+ import { ExpensiveReports } from "@/components/reports/expensive-reports";
14
+ import { HomeOverview } from "@/components/home/home-overview";
15
+ import { TabBar } from "@/components/navigation/tab-bar";
16
+
17
+ export function TabbedView() {
18
+ const [tab, setTab] = useState<"home" | "reports">("home");
19
+ const [isPending, startTransition] = useTransition();
20
+
21
+ function selectTab(next: "home" | "reports"): void {
22
+ startTransition(() => setTab(next));
23
+ }
24
+
25
+ return (
26
+ <>
27
+ <TabBar active={tab} onSelect={selectTab} busy={isPending} />
28
+ {tab === "reports" ? <ExpensiveReports /> : <HomeOverview />}
29
+ </>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ### React 19 async transitions and the post-`await` gotcha
35
+
36
+ React 19 allows `startTransition` to receive an async function (an "Action"). 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.
37
+
38
+ ```tsx
39
+ // ❌ BAD: setResult runs after await, outside the transition; isPending ends early.
40
+ startTransition(async () => {
41
+ const data = await fetchData();
42
+ setResult(data);
43
+ });
44
+
45
+ // ✅ GOOD: re-wrap the post-await update so it stays part of the transition.
46
+ startTransition(async () => {
47
+ const data = await fetchData();
48
+ startTransition(() => setResult(data));
49
+ });
50
+ ```
51
+
52
+ ## `startTransition` vs `useDeferredValue`
53
+
54
+ `startTransition` marks an update non-urgent at the **call site** (event handlers). `useDeferredValue` marks a **value** as allowed to lag when an expensive subtree trails a fast-changing input.
55
+
56
+ Prefer `useDeferredValue` over a fixed `setTimeout` debounce for expensive *derived renders*: it is interruptible and device-adaptive.
57
+
58
+ ```tsx
59
+ import { useDeferredValue, useState } from "react";
60
+
61
+ export interface RowItem {
62
+ id: string;
63
+ label: string;
64
+ }
65
+
66
+ export function FilterList({ rows }: { rows: RowItem[] }) {
67
+ const [text, setText] = useState<string>("");
68
+ const deferredText = useDeferredValue(text, "");
69
+
70
+ const filtered = rows.filter((r) => r.label.includes(deferredText));
71
+ const isStale = text !== deferredText;
72
+
73
+ return (
74
+ <>
75
+ <input value={text} onChange={(e) => setText(e.target.value)} />
76
+ <ul style={{ opacity: isStale ? 0.6 : 1 }}>
77
+ {filtered.map((r) => (
78
+ <li key={r.id}>{r.label}</li>
79
+ ))}
80
+ </ul>
81
+ </>
82
+ );
83
+ }
84
+ ```
85
+
86
+ ## Suspense for Data
87
+
88
+ A component suspends when it reads a not-yet-resolved promise via `use()`. The nearest `<Suspense fallback>` shows until the promise resolves.
89
+
90
+ ```tsx
91
+ import { Suspense, use } from "react";
92
+ import type { User } from "@/types/user.types";
93
+ import { ProfileSkeleton } from "@/components/profile/profile-skeleton";
94
+
95
+ export function ProfileSection({ userPromise }: { userPromise: Promise<User> }) {
96
+ return (
97
+ <Suspense fallback={<ProfileSkeleton />}>
98
+ <ProfileHeader userPromise={userPromise} />
99
+ </Suspense>
100
+ );
101
+ }
102
+
103
+ function ProfileHeader({ userPromise }: { userPromise: Promise<User> }) {
104
+ const user = use(userPromise);
105
+ return <h1>{user.name}</h1>;
106
+ }
107
+ ```
108
+
109
+ ### Resetting a boundary with `key`
110
+
111
+ Changing the `key` on a Suspense boundary (or a component inside it) discards the previous tree and re-suspends when switching the displayed resource:
112
+
113
+ ```tsx
114
+ <Suspense fallback={<ProfileSkeleton />}>
115
+ <ProfileLoader key={userId} userId={userId} />
116
+ </Suspense>
117
+ ```
118
+
119
+ ## Streaming SSR (React Level)
120
+
121
+ For Node servers, `renderToPipeableStream` streams HTML as Suspense boundaries resolve (`renderToReadableStream` for Web Streams / Edge / Cloudflare Workers). Design a meaningful **shell** (header, navigation, layout) that flushes immediately, and let slower data stream behind Suspense.
122
+
123
+ ## Anti-Patterns
124
+
125
+ | Anti-pattern | Why it is wrong | Detection |
126
+ |---|---|---|
127
+ | State update after `await` in an async transition | Escapes the transition; `isPending` ends early | UI flips to non-pending mid-operation |
128
+ | Fixed debounce/throttle for expensive derived renders | `useDeferredValue` is interruptible and adaptive | Janky typing on slow devices; hard-coded timers |
129
+ | One root spinner instead of a streamed shell | Blocks first paint on the slowest data | Long blank time-to-first-byte |
130
+ | Suspense fallback that mismatches final layout | Causes layout shift on reveal | Visible jump when content arrives |
@@ -0,0 +1,99 @@
1
+ # Performance Architecture
2
+
3
+ > Source of truth: [lazy](https://react.dev/reference/react/lazy) and [Suspense](https://react.dev/reference/react/Suspense).
4
+
5
+ With the React Compiler handling fine-grained memoization, performance engineering focuses on three architectural levers: rendering fewer DOM nodes (**virtualization**), shipping less JavaScript up front (**code-splitting**), and measuring before optimizing (**profiling**).
6
+
7
+ ## Virtualization
8
+
9
+ A list of thousands of rows mounts thousands of DOM nodes, degrading initial render and scroll responsiveness. Virtualization renders only the rows inside (and near) the viewport.
10
+
11
+ ### Library Comparison
12
+
13
+ | Library | Character | Use when |
14
+ |---|---|---|
15
+ | `@tanstack/react-virtual` | Modern, headless, strongest TypeScript support; full markup control | Default choice for custom layouts and strict TypeScript projects |
16
+ | `react-virtuoso` | Batteries-included (auto-sizing, grouping, sticky headers, chat feeds) | Rich variable-height list features out of the box |
17
+ | `react-window` | Legacy/minimal fixed-size lists | Maintaining existing codebases |
18
+
19
+ ```tsx
20
+ import { useRef } from "react";
21
+ import { useVirtualizer } from "@tanstack/react-virtual";
22
+ import type { LogEntry } from "@/types/log.types";
23
+ import { LogRowView } from "@/components/logs/log-row-view";
24
+
25
+ export function VirtualLogList({ rows }: { rows: LogEntry[] }) {
26
+ const parentRef = useRef<HTMLDivElement>(null);
27
+ const virtualizer = useVirtualizer({
28
+ count: rows.length,
29
+ getScrollElement: () => parentRef.current,
30
+ estimateSize: () => 40,
31
+ overscan: 8,
32
+ });
33
+
34
+ return (
35
+ <div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
36
+ <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
37
+ {virtualizer.getVirtualItems().map((virtualRow) => (
38
+ <div
39
+ key={virtualRow.key}
40
+ style={{
41
+ position: "absolute",
42
+ top: 0,
43
+ transform: `translateY(${virtualRow.start}px)`,
44
+ width: "100%",
45
+ }}
46
+ >
47
+ <LogRowView row={rows[virtualRow.index]} />
48
+ </div>
49
+ ))}
50
+ </div>
51
+ </div>
52
+ );
53
+ }
54
+ ```
55
+
56
+ **Keep row renderers light:** Once virtualized, per-item render cost becomes the bottleneck. Avoid heavy computation or deep subtrees inside `LogRowView`.
57
+
58
+ ## Code-Splitting
59
+
60
+ `React.lazy` + `Suspense` defers loading a component's bundle until it is rendered.
61
+
62
+ ```tsx
63
+ import { lazy, Suspense } from "react";
64
+ import { RouteSkeleton } from "@/components/layout/route-skeleton";
65
+
66
+ const AnalyticsDashboard = lazy(() => import("@/components/analytics/analytics-dashboard"));
67
+
68
+ export function AnalyticsRoute() {
69
+ return (
70
+ <Suspense fallback={<RouteSkeleton />}>
71
+ <AnalyticsDashboard />
72
+ </Suspense>
73
+ );
74
+ }
75
+ ```
76
+
77
+ ### Strategy
78
+
79
+ - **Split by route or heavy feature first** (charts, rich-text editors, admin modals).
80
+ - **Do not over-split** tiny (<10KB) components, which creates network request waterfalls.
81
+ - **Match fallback dimensions** to prevent Cumulative Layout Shift (CLS).
82
+
83
+ ## Profiling Workflow
84
+
85
+ 1. **Profile against a production/profiling build** (development builds add instrumentation overhead).
86
+ 2. **Record the interaction** in React DevTools Profiler and inspect the flamegraph and "Why did this render?".
87
+ 3. **Verify `"Memo ✨"` badges** to confirm the React Compiler optimized hot-path components.
88
+
89
+ ## Anti-Pattern → Detection → Fix Catalog
90
+
91
+ | Anti-pattern | Why it is wrong | How to detect | Fix |
92
+ |---|---|---|---|
93
+ | Hand-memoizing everything | Redundant with React Compiler | Compiler on, code cluttered with `useMemo` | Remove; keep only effect-stability escape hatches |
94
+ | Removing manual memo blindly | Breaks effect dependency identity contracts | `preserve-manual-memoization` lint warnings | Restore load-bearing memo |
95
+ | Large high-frequency context | Re-renders all consumers on every tick | Profiler highlights unrelated consumers | Split contexts by change frequency |
96
+ | `use()` inside `try / catch` or inline client `fetch()` | Swallows Suspense signals or loops infinitely | Infinite network requests or uncaught suspense | Use Error Boundary; hoist promise creation |
97
+ | State update after `await` in `startTransition` | Escapes transition; `isPending` ends early | Pending spinner disappears before UI updates | Re-wrap post-`await` setter in `startTransition` |
98
+ | Manual debounce for derived renders | Fixed delay on fast/slow devices alike | Input lag on low-end devices | Use `useDeferredValue` |
99
+ | Unvirtualized lists >100 rows | Mounts thousands of DOM nodes | Slow mount and scroll jank in Profiler | Virtualize with `@tanstack/react-virtual` |
@@ -0,0 +1,90 @@
1
+ # React Compiler — Adoption & Memoization Guidance
2
+
3
+ > Source of truth: [React Compiler docs](https://react.dev/learn/react-compiler/introduction) and the [1.0 announcement](https://react.dev/blog/2025/10/07/react-compiler-1).
4
+
5
+ ## What the Compiler Is
6
+
7
+ 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.
8
+
9
+ 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`).
10
+
11
+ ## The Memoization Inversion
12
+
13
+ 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).
14
+
15
+ ### Before (manual, pre-compiler)
16
+
17
+ ```tsx
18
+ export function Dashboard({ items, onSelect }: DashboardProps) {
19
+ const sorted = useMemo(() => items.slice().sort(byName), [items]);
20
+ const total = useMemo(() => items.reduce((s, i) => s + i.value, 0), [items]);
21
+ const handleSelect = useCallback((id: string): void => onSelect(id), [onSelect]);
22
+
23
+ return <ItemList items={sorted} total={total} onSelect={handleSelect} />;
24
+ }
25
+
26
+ const ItemList = React.memo(function ItemList({ items, total, onSelect }: ListProps) {
27
+ return /* ... */;
28
+ });
29
+ ```
30
+
31
+ ### After (compiler enabled)
32
+
33
+ ```tsx
34
+ // Plain code. The compiler memoizes `sorted`, `total`, `handleSelect`,
35
+ // and the ItemList render output as needed — including conditionally.
36
+ export function Dashboard({ items, onSelect }: DashboardProps) {
37
+ const sorted = items.slice().sort(byName);
38
+ const total = items.reduce((s, i) => s + i.value, 0);
39
+ const handleSelect = (id: string): void => onSelect(id);
40
+
41
+ return <ItemList items={sorted} total={total} onSelect={handleSelect} />;
42
+ }
43
+ ```
44
+
45
+ ## When Manual Memo Still Earns Its Place
46
+
47
+ Keep manual memo when:
48
+
49
+ 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.
50
+
51
+ ```tsx
52
+ const config = useMemo(() => ({ url, token }), [url, token]);
53
+ useEffect(() => subscribe(config), [config]);
54
+ ```
55
+
56
+ 2. **Code the compiler cannot statically analyze.** When the compiler cannot prove a component follows the Rules of React, it safely skips that component rather than risk a miscompile. In a skipped component, manual memo remains a valid local optimization.
57
+
58
+ ## Do Not Strip Existing Manual Memo Blindly
59
+
60
+ The compiler is designed to *preserve* existing `useMemo`/`useCallback`/`React.memo`. The [`preserve-manual-memoization`](https://react.dev/reference/eslint-plugin-react-hooks/lints/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.
61
+
62
+ ## Adoption Path
63
+
64
+ 1. **Install ESLint support:** `eslint-plugin-react-hooks` v6+ absorbed the former `eslint-plugin-react-compiler`.
65
+ 2. **Fix reported Rules of React violations** (mutations during render, conditional hooks).
66
+ 3. **Enable the compiler in the build** (Babel, Vite, Metro, Next.js, or Rsbuild).
67
+
68
+ ```ts
69
+ // vite.config.ts
70
+ import react from "@vitejs/plugin-react";
71
+
72
+ export default {
73
+ plugins: [
74
+ react({
75
+ babel: { plugins: [["babel-plugin-react-compiler", {}]] },
76
+ }),
77
+ ],
78
+ };
79
+ ```
80
+
81
+ 4. **Verify in DevTools:** React DevTools marks compiler-optimized components with a `"Memo ✨"` badge.
82
+
83
+ ## Detection Checklist
84
+
85
+ | Symptom | Likely cause | Action |
86
+ |---|---|---|
87
+ | New code still littered with `useMemo`/`useCallback` | Reflex memoization habit | Remove; let the compiler handle it |
88
+ | `preserve-manual-memoization` warnings after cleanup | Removed load-bearing memo | Restore the memo; it stabilized an effect/identity contract |
89
+ | A component shows no `"Memo ✨"` badge | Compiler skipped it (unanalyzable) | Fix Rules of React violations, or keep manual memo there |
90
+ | Effect re-fires every render | A dependency value is recreated each render | Memoize that single dependency by hand |
@@ -0,0 +1,145 @@
1
+ # React 19 Platform Hooks & APIs
2
+
3
+ > Source of truth: [use()](https://react.dev/reference/react/use), [useActionState](https://react.dev/reference/react/useActionState), [useOptimistic](https://react.dev/reference/react/useOptimistic), [useFormStatus](https://react.dev/reference/react-dom/hooks/useFormStatus), and [<form>](https://react.dev/reference/react-dom/components/form).
4
+
5
+ ## The `use()` Hook
6
+
7
+ `use()` reads the value of a promise or a context. Unlike every other hook, it may be called **conditionally** — after early returns or inside branches. When given a promise, it suspends the component until the promise resolves, integrating with the nearest Suspense boundary.
8
+
9
+ ```tsx
10
+ import { use } from "react";
11
+ import type { CommentItem } from "@/types/comment.types";
12
+
13
+ export function CommentList({
14
+ commentsPromise,
15
+ isVisible,
16
+ }: {
17
+ commentsPromise: Promise<CommentItem[]>;
18
+ isVisible: boolean;
19
+ }) {
20
+ if (!isVisible) {
21
+ return null;
22
+ }
23
+
24
+ // Legal after an early return — `use` is exempt from the top-level rule.
25
+ const comments = use(commentsPromise);
26
+ return (
27
+ <ul>
28
+ {comments.map((c) => (
29
+ <li key={c.id}>{c.text}</li>
30
+ ))}
31
+ </ul>
32
+ );
33
+ }
34
+ ```
35
+
36
+ ### Rules and pitfalls
37
+
38
+ - **Never wrap `use()` in `try / catch`.** A suspended promise is not a synchronous error; rejections surface through an **Error Boundary**, not `catch`.
39
+ - **Do not create the promise during render on the client.** A promise created in the render body is recreated every render, causing infinite suspension loops. Create it on the server, in a parent loader, or in an event handler, and pass it down.
40
+
41
+ ## Actions (`useActionState`, `useOptimistic`, `useFormStatus`)
42
+
43
+ Actions are React's built-in async-mutation model, providing pending, error, and optimistic handling at the React level.
44
+
45
+ ### `useActionState`
46
+
47
+ Runs an action function, tracks `isPending`, and threads the previous result forward (replaces the canary `useFormState`).
48
+
49
+ ```tsx
50
+ import { useActionState } from "react";
51
+
52
+ interface SaveResult {
53
+ error?: string;
54
+ }
55
+
56
+ export function UpdateNameForm({
57
+ saveName,
58
+ }: {
59
+ saveName: (name: string) => Promise<SaveResult>;
60
+ }) {
61
+ const [state, formAction, isPending] = useActionState<SaveResult, FormData>(
62
+ async (_prev, formData) => {
63
+ const name = String(formData.get("name") ?? "");
64
+ return await saveName(name);
65
+ },
66
+ {}
67
+ );
68
+
69
+ return (
70
+ <form action={formAction}>
71
+ <input name="name" disabled={isPending} />
72
+ <button type="submit" disabled={isPending}>
73
+ {isPending ? "Saving..." : "Save"}
74
+ </button>
75
+ {state.error && <p role="alert">{state.error}</p>}
76
+ </form>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ### `useOptimistic`
82
+
83
+ Renders an optimistic value while an async action is in flight, reverting automatically when the action settles.
84
+
85
+ ```tsx
86
+ import { useOptimistic } from "react";
87
+
88
+ export function LikeButton({
89
+ count,
90
+ onLike,
91
+ }: {
92
+ count: number;
93
+ onLike: () => Promise<void>;
94
+ }) {
95
+ const [optimisticCount, addOptimistic] = useOptimistic(
96
+ count,
97
+ (current, delta: number) => current + delta
98
+ );
99
+
100
+ async function handleAction(): Promise<void> {
101
+ addOptimistic(1);
102
+ await onLike();
103
+ }
104
+
105
+ return (
106
+ <form action={handleAction}>
107
+ <button type="submit">♥ {optimisticCount}</button>
108
+ </form>
109
+ );
110
+ }
111
+ ```
112
+
113
+ ### `useFormStatus`
114
+
115
+ Reads the pending state of the enclosing `<form>` from a child component without prop drilling.
116
+
117
+ ```tsx
118
+ import { useFormStatus } from "react-dom";
119
+
120
+ export function SubmitButton({ label }: { label: string }) {
121
+ const { pending } = useFormStatus();
122
+ return (
123
+ <button type="submit" disabled={pending}>
124
+ {pending ? "Submitting..." : label}
125
+ </button>
126
+ );
127
+ }
128
+ ```
129
+
130
+ ## React 19 Ergonomic Changes
131
+
132
+ 1. **`ref` as a plain prop**: `forwardRef` is deprecated. Derive wrapper props from `ComponentProps<"input">` (which already includes `ref`), or add `ref?: React.Ref<HTMLElement>` to custom interfaces.
133
+ 2. **`<Context value>` as provider**: Render `<ThemeContext value={theme}>{children}</ThemeContext>` instead of `<ThemeContext.Provider>`.
134
+ 3. **Native document metadata**: Render `<title>`, `<meta>`, and `<link>` directly inside components; React hoists them to `<head>` automatically without `react-helmet`.
135
+
136
+ ## Migration Notes from React 17/18
137
+
138
+ | React 17/18 idiom | React 19 replacement |
139
+ |---|---|
140
+ | `forwardRef((props, ref) => ...)` | `ref` as a plain prop |
141
+ | `<Context.Provider value={...}>` | `<Context value={...}>` |
142
+ | `react-helmet` for `<head>` tags | Native `<title>`, `<meta>`, `<link>` hoisting |
143
+ | `useFormState` (canary) | `useActionState` (from `"react"`) |
144
+ | Manual `isLoading`/`error` state in submit handlers | `useActionState` + `useOptimistic` + `useFormStatus` |
145
+ | Fetch in `useEffect` + store in `useState` | Pass a promise to `use()` under `<Suspense>` |