
React Syntax Hooks Advanced
- 11 installs
- 6 repo stars
- Updated July 8, 2026
- openaec-foundation/react-claude-skill-package
Helps with frontend development tasks.
About
react-syntax-hooks-advanced is a Claude Code skill for frontend development. It helps solo builders move faster with AI-assisted development.
- react-syntax-hooks-advanced
- Frontend Development
- AI-coding skill
React Syntax Hooks Advanced by the numbers
- 11 all-time installs (skills.sh)
- Ranked #1,658 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/react-claude-skill-package --skill react-syntax-hooks-advancedAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/react-claude-skill-package ↗ |
What it does
Helps with frontend development tasks.
Files
react-syntax-hooks-advanced
Quick Reference
Advanced Hooks at a Glance
| Hook | Version | Purpose | Returns |
|---|---|---|---|
useId | 18+ | SSR-safe unique IDs for accessibility | string |
useTransition | 18+ | Mark state updates as non-blocking | [isPending, startTransition] |
useDeferredValue | 18+ | Defer expensive re-renders | T (deferred value) |
useSyncExternalStore | 18+ | Subscribe to external stores | T (store snapshot) |
useInsertionEffect | 18+ | Inject styles before DOM mutations | void |
useDebugValue | 18+ | Label custom hooks in DevTools | void |
use | 19+ | Read promises (Suspense) and context conditionally | T |
useActionState | 19+ | Manage async action state with pending flag | [state, dispatch, isPending] |
useOptimistic | 19+ | Optimistic UI during async actions | [optimisticState, setOptimistic] |
useFormStatus | 19+ | Read parent form submission state | {pending, data, method, action} |
Critical Warnings
NEVER use useId to generate keys for list items -- keys MUST come from your data. useId generates IDs for accessibility attributes only.
NEVER use useInsertionEffect in application code -- it exists ONLY for CSS-in-JS library authors. Use useEffect or useLayoutEffect instead.
NEVER call useFormStatus in the same component that renders the <form> -- it MUST be called from a child component rendered inside the form.
NEVER call use() inside a try/catch block -- use an Error Boundary for error handling with promises.
NEVER wrap controlled text input updates in startTransition -- transitions are interruptible and will cause the input to feel broken.
---
Decision Tree: Which Advanced Hook?
Need a unique ID for aria/htmlFor?
YES --> useId
Need to keep UI responsive during expensive state update?
YES --> Do you have the state setter?
YES --> useTransition (wrap setState in startTransition)
NO --> useDeferredValue (defer the prop/value)
Need to subscribe to non-React state (Redux, browser API, etc.)?
YES --> useSyncExternalStore
Need to inject <style> tags before DOM mutations?
YES --> Are you building a CSS-in-JS library?
YES --> useInsertionEffect
NO --> useEffect or useLayoutEffect
Need to read a Promise with Suspense? (React 19)
YES --> use(promise)
Need to read context conditionally? (React 19)
YES --> use(context)
Need form action with pending state and error handling? (React 19)
YES --> useActionState
Need instant UI feedback before async action completes? (React 19)
YES --> useOptimistic
Need form submission status in a child component? (React 19)
YES --> useFormStatus---
React 18 Advanced Hooks
useId
Generates a unique string ID that is stable across server and client renders.
import { useId } from 'react';
function EmailField(): JSX.Element {
const id = useId();
return (
<div>
<label htmlFor={id + '-email'}>Email</label>
<input id={id + '-email'} type="email" aria-describedby={id + '-hint'} />
<p id={id + '-hint'}>We will never share your email.</p>
</div>
);
}ALWAYS use useId as a prefix when generating multiple related IDs. ALWAYS pass identifierPrefix to the root when running multiple React apps on the same page.
---
useTransition
Marks state updates as non-urgent so they do not block user interaction.
import { useState, useTransition } from 'react';
function TabContainer(): JSX.Element {
const [tab, setTab] = useState<string>('home');
const [isPending, startTransition] = useTransition();
function selectTab(nextTab: string): void {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<button onClick={() => selectTab('about')}>About</button>
{isPending && <span>Loading...</span>}
<TabPanel tab={tab} />
</div>
);
}startTransition executes its callback immediately -- the state update inside is what becomes interruptible. ALWAYS wrap startTransition inside setTimeout, NEVER the other way around. State updates after await inside an async startTransition require a nested startTransition call.
---
useDeferredValue
Returns a deferred copy of a value that lags behind during urgent updates.
import { useDeferredValue, useMemo } from 'react';
function SearchResults({ query }: { query: string }): JSX.Element {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const results = useMemo(() => filterResults(deferredQuery), [deferredQuery]);
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<ResultsList items={results} />
</div>
);
}React 19+ adds an optional initialValue parameter:```tsx
const deferredQuery = useDeferredValue(query, ''); // '' on first render
```
ALWAYS use primitives or objects created outside the render function. Passing a new object every render defeats the purpose of deferral.
---
useSyncExternalStore
Subscribes a component to an external (non-React) data source with tear-free reads.
import { useSyncExternalStore } from 'react';
function useOnlineStatus(): boolean {
return useSyncExternalStore(
subscribe,
() => navigator.onLine, // client snapshot
() => true // server snapshot (SSR)
);
}
function subscribe(callback: () => void): () => void {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}ALWAYS declare the subscribe function outside the component or wrap it in useCallback -- a new function reference causes resubscription every render. getSnapshot MUST return immutable data; returning a new object every call causes an infinite re-render loop.
---
useInsertionEffect
Fires before React makes any DOM changes. Reserved for CSS-in-JS libraries.
import { useInsertionEffect } from 'react';
// ONLY for CSS-in-JS library authors
function useCSS(rule: string): string {
useInsertionEffect(() => {
const style = document.createElement('style');
style.textContent = rule;
document.head.appendChild(style);
return () => { document.head.removeChild(style); };
});
return rule;
}Cannot call setState inside. Refs are NOT attached yet. Does NOT run during SSR.
---
useDebugValue
Adds a label to custom hooks visible in React DevTools.
import { useDebugValue } from 'react';
function useOnlineStatus(): boolean {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
useDebugValue(isOnline ? 'Online' : 'Offline');
return isOnline;
}ALWAYS pass a formatter function for expensive formatting to avoid cost when DevTools is closed:
useDebugValue(date, (d: Date) => d.toISOString());---
React 19 Hooks
use()
React 19+
Reads a Promise (triggering Suspense) or a Context value. Unlike all other hooks, use() CAN be called inside conditionals, loops, and after early returns.
import { use, Suspense } from 'react';
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }): JSX.Element {
const comments = use(commentsPromise);
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
// Conditional context reading -- impossible with useContext
function Heading({ children }: { children: React.ReactNode | null }): JSX.Element | null {
if (children == null) return null;
const theme = use(ThemeContext);
return <h1 style={{ color: theme.color }}>{children}</h1>;
}ALWAYS create promises in Server Components and pass them to Client Components -- promises created during client render are recreated every render. ALWAYS wrap the consuming component in <Suspense> and <ErrorBoundary>.
---
useActionState
React 19+
Manages state from form actions with built-in pending tracking. Replaces the deprecated useFormState.
import { useActionState } from 'react';
async function submitForm(
prevState: { error: string | null },
formData: FormData
): Promise<{ error: string | null }> {
const name = formData.get('name') as string;
if (!name) return { error: 'Name is required' };
await saveToDatabase(name);
return { error: null };
}
function ContactForm(): JSX.Element {
const [state, dispatch, isPending] = useActionState(submitForm, { error: null });
return (
<form action={dispatch}>
<input name="name" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
{state.error && <p className="error">{state.error}</p>}
</form>
);
}Multiple dispatches are queued sequentially -- each receives the previous action's return value. Errors cancel all queued actions and trigger the nearest Error Boundary.
---
useOptimistic
React 19+
Shows an optimistic state while an async action is in progress. Automatically reverts when the action completes.
import { useOptimistic, startTransition } from 'react';
function LikeButton({ liked, onToggle }: {
liked: boolean;
onToggle: (next: boolean) => Promise<void>;
}): JSX.Element {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);
function handleClick(): void {
startTransition(async () => {
setOptimisticLiked(!optimisticLiked);
await onToggle(!optimisticLiked);
});
}
return <button onClick={handleClick}>{optimisticLiked ? 'Unlike' : 'Like'}</button>;
}setOptimistic MUST be called inside a startTransition or an Action prop. The reducer (if provided) MUST be pure -- no side effects.
---
useFormStatus
React 19+
Reads the submission status of the nearest parent <form>. Imported from react-dom.
import { useFormStatus } from 'react-dom';
function SubmitButton(): JSX.Element {
const { pending, data } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function MyForm(): JSX.Element {
return (
<form action={handleSubmit}>
<input name="email" type="email" />
<SubmitButton /> {/* MUST be a child of <form> */}
</form>
);
}Returns { pending, data, method, action }. pending is ALWAYS false when called outside a <form> or in the same component that renders the <form>.
---
Reference Links
- references/examples.md -- Advanced hook patterns and compositions
- references/api-table.md -- Complete hook signatures with TypeScript types
- references/anti-patterns.md -- What NOT to do with advanced hooks
Official Sources
- https://react.dev/reference/react/useId
- https://react.dev/reference/react/useTransition
- https://react.dev/reference/react/useDeferredValue
- https://react.dev/reference/react/useSyncExternalStore
- https://react.dev/reference/react/useInsertionEffect
- https://react.dev/reference/react/useDebugValue
- 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
Advanced Hooks — Anti-Patterns
useId Anti-Patterns
NEVER use useId for list keys
// WRONG — useId is for accessibility, not list rendering
function TodoList({ items }: { items: string[] }): JSX.Element {
return (
<ul>
{items.map(item => {
const id = useId(); // BREAKS: hooks in loops
return <li key={id}>{item}</li>;
})}
</ul>
);
}
// CORRECT — keys come from data
function TodoList({ items }: { items: { id: string; text: string }[] }): JSX.Element {
return (
<ul>
{items.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}Why: useId cannot be called inside loops (Rules of Hooks). Keys MUST be derived from your data, not generated per render.
NEVER use useId for cache keys or data identifiers
// WRONG — useId output changes between server/client in unpredictable ways
const cacheKey = useId(); // Not designed for this purpose
localStorage.setItem(cacheKey, data);
// CORRECT — use crypto.randomUUID() or data-derived keys
const cacheKey = `user-${userId}-preferences`;---
useTransition Anti-Patterns
NEVER wrap controlled input updates in startTransition
// WRONG — input becomes unresponsive
function Search(): JSX.Element {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
startTransition(() => {
setQuery(e.target.value); // Transition makes typing laggy
});
}
return <input value={query} onChange={handleChange} />;
}
// CORRECT — update input immediately, defer expensive work
function Search(): JSX.Element {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState<Item[]>([]);
function handleChange(e: React.ChangeEvent<HTMLInputElement>): void {
setQuery(e.target.value); // Immediate update
startTransition(() => {
setResults(filterItems(e.target.value)); // Deferred work
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending ? <Spinner /> : <ResultList items={results} />}
</div>
);
}Why: Transitions are interruptible. Wrapping a controlled input update in a transition causes the input to revert characters while typing.
NEVER nest startTransition inside setTimeout
// WRONG — loses transition context
startTransition(() => {
setTimeout(() => {
setState(newValue); // NOT in a transition anymore
}, 1000);
});
// CORRECT — wrap startTransition inside setTimeout
setTimeout(() => {
startTransition(() => {
setState(newValue); // Properly marked as transition
});
}, 1000);Why: JavaScript async boundaries (setTimeout, Promise.then) exit the startTransition scope.
NEVER forget nested startTransition after await
// WRONG — updates after await lose transition context
startTransition(async () => {
const data = await fetchData();
setState(data); // NOT in transition after await in React 18
});
// CORRECT — re-wrap after await
startTransition(async () => {
const data = await fetchData();
startTransition(() => {
setState(data); // Properly marked
});
});---
useDeferredValue Anti-Patterns
NEVER pass new objects created during render
// WRONG — new object every render defeats deferral
function SearchResults({ query }: { query: string }): JSX.Element {
const deferredOptions = useDeferredValue({ query, limit: 10 }); // New object every render
return <Results options={deferredOptions} />;
}
// CORRECT — defer a primitive or stable reference
function SearchResults({ query }: { query: string }): JSX.Element {
const deferredQuery = useDeferredValue(query); // Primitive — stable comparison
const options = useMemo(() => ({ query: deferredQuery, limit: 10 }), [deferredQuery]);
return <Results options={options} />;
}Why: useDeferredValue compares with Object.is. A new object every render is always "different", so deferral never kicks in.
---
useSyncExternalStore Anti-Patterns
NEVER create subscribe function inline
// WRONG — new function every render causes resubscription loop
function OnlineStatus(): JSX.Element {
const isOnline = useSyncExternalStore(
(callback) => { // New function reference each render
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
},
() => navigator.onLine
);
return <span>{isOnline ? 'Online' : 'Offline'}</span>;
}
// CORRECT — stable subscribe function declared outside component
function subscribe(callback: () => void): () => void {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function OnlineStatus(): JSX.Element {
const isOnline = useSyncExternalStore(subscribe, () => navigator.onLine, () => true);
return <span>{isOnline ? 'Online' : 'Offline'}</span>;
}Why: A different subscribe reference causes React to unsubscribe and resubscribe every render, wasting resources and potentially causing missed updates.
NEVER return new objects from getSnapshot
// WRONG — infinite re-render loop
function useWindowSize() {
return useSyncExternalStore(
subscribe,
() => ({ width: window.innerWidth, height: window.innerHeight }), // New object every call
);
}
// CORRECT — return cached object, update only on change
let cachedSize = { width: 0, height: 0 };
function getSnapshot(): { width: number; height: number } {
const next = { width: window.innerWidth, height: window.innerHeight };
if (next.width !== cachedSize.width || next.height !== cachedSize.height) {
cachedSize = next;
}
return cachedSize;
}Why: getSnapshot is called frequently. If it returns a new object every time, Object.is comparison always returns false, triggering infinite re-renders.
---
useInsertionEffect Anti-Patterns
NEVER use useInsertionEffect in application code
// WRONG — useInsertionEffect is for CSS-in-JS libraries only
function MyComponent(): JSX.Element {
useInsertionEffect(() => {
document.title = 'Hello'; // Use useEffect for this
});
return <div />;
}
// CORRECT — use useEffect for side effects
function MyComponent(): JSX.Element {
useEffect(() => {
document.title = 'Hello';
});
return <div />;
}Why: useInsertionEffect cannot update state, refs are not attached, and it runs before DOM mutations. It exists solely for injecting <style> tags in CSS-in-JS libraries.
---
use() Anti-Patterns (React 19)
NEVER call use() inside try/catch
// WRONG — causes "Suspense Exception" error
function UserProfile({ userPromise }: { userPromise: Promise<User> }): JSX.Element {
try {
const user = use(userPromise); // Throws internal Suspense exception
return <div>{user.name}</div>;
} catch (e) {
return <div>Error</div>;
}
}
// CORRECT — use ErrorBoundary for error handling
function UserProfile({ userPromise }: { userPromise: Promise<User> }): JSX.Element {
const user = use(userPromise);
return <div>{user.name}</div>;
}
// Wrap with ErrorBoundary and Suspense
<ErrorBoundary fallback={<div>Error loading user</div>}>
<Suspense fallback={<div>Loading...</div>}>
<UserProfile userPromise={fetchUser(id)} />
</Suspense>
</ErrorBoundary>Why: use() throws a special Suspense exception internally. Catching it with try/catch breaks React's Suspense mechanism.
NEVER create promises during client render
// WRONG — new promise every render causes repeated suspense
function Comments({ postId }: { postId: string }): JSX.Element {
const comments = use(fetchComments(postId)); // New promise every render!
return <ul>{comments.map(c => <li key={c.id}>{c.text}</li>)}</ul>;
}
// CORRECT — create promise in Server Component or cache it
// Server Component creates the promise once
async function Page({ postId }: { postId: string }) {
const commentsPromise = fetchComments(postId); // Created once on server
return (
<Suspense fallback={<div>Loading...</div>}>
<Comments commentsPromise={commentsPromise} />
</Suspense>
);
}---
useFormStatus Anti-Patterns (React 19)
NEVER call useFormStatus in the form component itself
// WRONG — pending is ALWAYS false
function LoginForm(): JSX.Element {
const { pending } = useFormStatus(); // Same component as <form>
return (
<form action={loginAction}>
<input name="email" />
<button disabled={pending}>Log in</button>
</form>
);
}
// CORRECT — extract button to a child component
function SubmitButton(): JSX.Element {
const { pending } = useFormStatus(); // Inside <form> as a child
return <button type="submit" disabled={pending}>{pending ? 'Logging in...' : 'Log in'}</button>;
}
function LoginForm(): JSX.Element {
return (
<form action={loginAction}>
<input name="email" />
<SubmitButton />
</form>
);
}Why: useFormStatus reads the status of the parent <form>. When called in the same component that renders the form, there is no parent form.
---
useOptimistic Anti-Patterns (React 19)
NEVER call setOptimistic outside a Transition
// WRONG — optimistic value flashes and immediately reverts
function LikeButton({ liked }: { liked: boolean }): JSX.Element {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);
function handleClick(): void {
setOptimisticLiked(!liked); // Not inside startTransition — reverts immediately
toggleLike(!liked);
}
return <button onClick={handleClick}>{optimisticLiked ? 'Liked' : 'Like'}</button>;
}
// CORRECT — wrap in startTransition
function LikeButton({ liked, onToggle }: {
liked: boolean;
onToggle: (v: boolean) => Promise<void>;
}): JSX.Element {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);
function handleClick(): void {
startTransition(async () => {
setOptimisticLiked(!liked);
await onToggle(!liked);
});
}
return <button onClick={handleClick}>{optimisticLiked ? 'Liked' : 'Like'}</button>;
}Why: The optimistic state only persists while an Action/Transition is in progress. Without a transition, the optimistic value is applied and immediately replaced by the real value.
---
useActionState Anti-Patterns (React 19)
NEVER confuse useActionState with useReducer
// WRONG — treating action like a pure reducer
const [state, dispatch] = useActionState(
(prev: number, action: { type: string }) => {
// Side effects are allowed here, unlike useReducer
// But returning wrong type causes issues
if (action.type === 'INCREMENT') return prev + 1;
return prev; // Must return State, not void
},
0
);
// Using dispatch outside a form without startTransition
dispatch({ type: 'INCREMENT' }); // May not work as expected
// CORRECT — use with form action or wrap in startTransition
<form action={() => dispatch({ type: 'INCREMENT' })}>
<button type="submit">+1</button>
</form>
// OR
startTransition(() => {
dispatch({ type: 'INCREMENT' });
});Why: useActionState dispatches MUST be called from within a startTransition or passed as a form action. Direct calls outside these contexts do not trigger the pending state correctly.
Advanced Hooks — API Reference Table
React 18+ Hooks
useId
function useId(): string;| Aspect | Detail |
|---|---|
| Parameters | None |
| Returns | string — unique ID stable across re-renders and SSR hydration |
| Re-render trigger | Never (ID is stable) |
| SSR behavior | Generates matching IDs on server and client |
---
useTransition
function useTransition(): [
isPending: boolean,
startTransition: (action: () => void | Promise<void>) => void
];| Aspect | Detail |
|---|---|
| Parameters | None |
| Returns | [isPending, startTransition] |
isPending | true while transition is in progress |
startTransition | Stable identity; callback executes immediately |
| Re-render trigger | When isPending changes |
---
useDeferredValue
// React 18
function useDeferredValue<T>(value: T): T;
// React 19
function useDeferredValue<T>(value: T, initialValue?: T): T;| Aspect | Detail |
|---|---|
value | Any type — the value to defer |
initialValue | (React 19 only) Value used on initial render |
| Returns | T — deferred copy that lags behind during urgent updates |
| Comparison | Object.is to detect changes |
| Re-render trigger | Two-phase: immediate with old value, then background with new value |
---
useSyncExternalStore
function useSyncExternalStore<T>(
subscribe: (callback: () => void) => () => void,
getSnapshot: () => T,
getServerSnapshot?: () => T
): T;| Parameter | Type | Requirement |
|---|---|---|
subscribe | (callback: () => void) => () => void | MUST be stable (outside component or useCallback) |
getSnapshot | () => T | MUST return immutable/cached data |
getServerSnapshot | () => T (optional) | MUST match between server and client hydration |
| Aspect | Detail |
|---|---|
| Returns | T — current store snapshot |
| Re-render trigger | When getSnapshot() returns different value (via Object.is) |
---
useInsertionEffect
function useInsertionEffect(
setup: () => (void | (() => void)),
dependencies?: ReadonlyArray<unknown>
): void;| Aspect | Detail |
|---|---|
| Timing | Before DOM mutations (before useLayoutEffect) |
setState | NOT allowed inside |
| Refs | NOT attached yet |
| SSR | Does NOT run |
| Intended users | CSS-in-JS library authors ONLY |
---
useDebugValue
function useDebugValue<T>(value: T, format?: (value: T) => any): void;| Parameter | Type | Description |
|---|---|---|
value | T | Value shown in React DevTools |
format | (value: T) => any (optional) | Lazy formatter; only called when inspected |
| Aspect | Detail |
|---|---|
| Returns | void |
| Purpose | Label custom hooks in DevTools |
| Performance | Formatter defers computation until inspection |
---
React 19 Hooks
use
function use<T>(resource: Promise<T>): T;
function use<T>(context: React.Context<T>): T;| Resource Type | Behavior |
|---|---|
Promise<T> | Suspends component until resolved; integrates with <Suspense> |
React.Context<T> | Returns context value; CAN be called conditionally |
| Aspect | Detail |
|---|---|
| Conditionals | ALLOWED (unique among hooks) |
| Loops | ALLOWED |
try/catch | NOT allowed — use Error Boundary |
| Promise errors | Caught by nearest Error Boundary |
---
useActionState
function useActionState<State>(
action: (state: State, payload: unknown) => State | Promise<State>,
initialState: State,
permalink?: string
): [state: State, dispatch: (payload: unknown) => void, isPending: boolean];| Parameter | Type | Description |
|---|---|---|
action | `(state: S, payload: P) => S \ | Promise<S>` |
initialState | S | Initial state; MUST be serializable for Server Functions |
permalink | string (optional) | URL for progressive enhancement before JS loads |
| Return | Type | Description |
|---|---|---|
state | S | Current state |
dispatch | (payload: P) => void | Triggers action; stable identity |
isPending | boolean | true while action executes |
---
useOptimistic
function useOptimistic<State, Action>(
passthrough: State,
reducer?: (currentState: State, action: Action) => State
): [State, (action: Action) => void];| Parameter | Type | Description |
|---|---|---|
passthrough | State | Real state; returned when no Action is pending |
reducer | (current: S, action: A) => S (optional) | Pure function to compute optimistic state |
| Return | Type | Description |
|---|---|---|
optimisticState | State | Optimistic value during action; real value otherwise |
setOptimistic | (action: Action) => void | MUST be called inside startTransition or Action |
---
useFormStatus
// Import from 'react-dom', NOT 'react'
function useFormStatus(): {
pending: boolean;
data: FormData | null;
method: 'get' | 'post';
action: ((...args: any[]) => any) | null;
};| Property | Type | Description |
|---|---|---|
pending | boolean | true when parent <form> is submitting |
data | `FormData \ | null` |
method | `'get' \ | 'post'` |
action | `function \ | null` |
| Aspect | Detail |
|---|---|
| Parameters | None |
| Requirement | Component MUST be rendered inside a <form> |
| Import | import { useFormStatus } from 'react-dom' |
---
Effect Execution Order
| Hook | Fires | Blocks Paint | Use Case |
|---|---|---|---|
useInsertionEffect | Before DOM mutations | Yes | CSS-in-JS style injection |
useLayoutEffect | After DOM mutations, before paint | Yes | DOM measurement |
useEffect | After paint | No | Side effects, subscriptions |
Advanced Hooks — Patterns and Examples
Pattern: Accessible Form Fields with useId
import { useId } from 'react';
interface FieldProps {
label: string;
type?: string;
error?: string;
}
function FormField({ label, type = 'text', error }: FieldProps): JSX.Element {
const id = useId();
const inputId = id + '-input';
const errorId = id + '-error';
const hintId = id + '-hint';
return (
<div>
<label htmlFor={inputId}>{label}</label>
<input
id={inputId}
type={type}
aria-describedby={error ? errorId : hintId}
aria-invalid={!!error}
/>
{error && <p id={errorId} role="alert">{error}</p>}
</div>
);
}---
Pattern: Search with Deferred Results
Keeps the text input responsive while deferring expensive filtering.
import { useState, useDeferredValue, useMemo } from 'react';
interface Product {
id: string;
name: string;
category: string;
}
function ProductSearch({ products }: { products: Product[] }): JSX.Element {
const [query, setQuery] = useState<string>('');
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filteredProducts = useMemo(
() => products.filter(p =>
p.name.toLowerCase().includes(deferredQuery.toLowerCase())
),
[products, deferredQuery]
);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
<div style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.2s' }}>
{filteredProducts.map(p => <ProductCard key={p.id} product={p} />)}
</div>
</div>
);
}---
Pattern: Tab Navigation with useTransition
Prevents UI jank when switching between tabs with expensive content.
import { useState, useTransition, Suspense } from 'react';
type Tab = 'posts' | 'comments' | 'photos';
function TabContainer(): JSX.Element {
const [tab, setTab] = useState<Tab>('posts');
const [isPending, startTransition] = useTransition();
function handleTabChange(nextTab: Tab): void {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<nav>
{(['posts', 'comments', 'photos'] as Tab[]).map(t => (
<button
key={t}
onClick={() => handleTabChange(t)}
style={{ fontWeight: tab === t ? 'bold' : 'normal' }}
>
{t}
</button>
))}
</nav>
{isPending && <div className="spinner" />}
<Suspense fallback={<div>Loading tab...</div>}>
<TabContent tab={tab} />
</Suspense>
</div>
);
}---
Pattern: External Store Subscription
Subscribe to browser APIs or third-party state without tearing.
import { useSyncExternalStore, useCallback } from 'react';
// Store: window dimensions
function subscribe(callback: () => void): () => void {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
}
function getSnapshot(): { width: number; height: number } {
// ALWAYS return a cached/memoized object to avoid infinite re-renders
return windowSize;
}
let windowSize = { width: window.innerWidth, height: window.innerHeight };
window.addEventListener('resize', () => {
windowSize = { width: window.innerWidth, height: window.innerHeight };
});
function getServerSnapshot(): { width: number; height: number } {
return { width: 1024, height: 768 }; // sensible default for SSR
}
function useWindowSize(): { width: number; height: number } {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}
function ResponsiveLayout(): JSX.Element {
const { width } = useWindowSize();
return width > 768 ? <DesktopLayout /> : <MobileLayout />;
}---
Pattern: Generic External Store Hook
import { useSyncExternalStore } from 'react';
interface Store<T> {
getState(): T;
subscribe(listener: () => void): () => void;
}
function useStore<T>(store: Store<T>): T {
return useSyncExternalStore(
store.subscribe,
store.getState,
store.getState // server snapshot
);
}
function useStoreSelector<T, S>(store: Store<T>, selector: (state: T) => S): S {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
() => selector(store.getState())
);
}---
Pattern: Custom Hook with useDebugValue
import { useState, useEffect, useDebugValue } from 'react';
interface FetchState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useDebugValue(state.loading ? 'Loading' : state.error ? 'Error' : 'Ready');
useEffect(() => {
let ignore = false;
setState({ data: null, loading: true, error: null });
fetch(url)
.then(res => res.json())
.then((data: T) => {
if (!ignore) setState({ data, loading: false, error: null });
})
.catch((error: Error) => {
if (!ignore) setState({ data: null, loading: false, error });
});
return () => { ignore = true; };
}, [url]);
return state;
}---
React 19 Patterns
Pattern: use() with Promises and Suspense
import { use, Suspense } from 'react';
interface Article {
id: string;
title: string;
content: string;
}
// Promise created in Server Component and passed as prop
function ArticleContent({ articlePromise }: { articlePromise: Promise<Article> }): JSX.Element {
const article = use(articlePromise);
return (
<article>
<h1>{article.title}</h1>
<p>{article.content}</p>
</article>
);
}
function ArticlePage({ articlePromise }: { articlePromise: Promise<Article> }): JSX.Element {
return (
<Suspense fallback={<div>Loading article...</div>}>
<ArticleContent articlePromise={articlePromise} />
</Suspense>
);
}Pattern: use() with Conditional Context
import { use, createContext } from 'react';
const ThemeContext = createContext<{ dark: boolean }>({ dark: false });
const AuthContext = createContext<{ isAdmin: boolean }>({ isAdmin: false });
function AdminPanel({ show }: { show: boolean }): JSX.Element | null {
if (!show) return null;
// Conditional context reading -- ONLY possible with use(), not useContext
const { isAdmin } = use(AuthContext);
if (!isAdmin) return null;
const theme = use(ThemeContext);
return <div style={{ background: theme.dark ? '#333' : '#fff' }}>Admin Panel</div>;
}---
Pattern: Form Action with useActionState
import { useActionState } from 'react';
interface FormState {
message: string | null;
errors: Record<string, string>;
}
async function createUser(
prevState: FormState,
formData: FormData
): Promise<FormState> {
const email = formData.get('email') as string;
const name = formData.get('name') as string;
const errors: Record<string, string> = {};
if (!email) errors.email = 'Email is required';
if (!name) errors.name = 'Name is required';
if (Object.keys(errors).length > 0) return { message: null, errors };
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ email, name }),
});
return { message: 'User created successfully', errors: {} };
}
function UserForm(): JSX.Element {
const [state, dispatch, isPending] = useActionState(createUser, {
message: null,
errors: {},
});
return (
<form action={dispatch}>
<div>
<input name="name" placeholder="Name" disabled={isPending} />
{state.errors.name && <span className="error">{state.errors.name}</span>}
</div>
<div>
<input name="email" type="email" placeholder="Email" disabled={isPending} />
{state.errors.email && <span className="error">{state.errors.email}</span>}
</div>
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
{state.message && <p className="success">{state.message}</p>}
</form>
);
}---
Pattern: Optimistic Todo List
import { useOptimistic, startTransition } from 'react';
interface Todo {
id: string;
text: string;
pending?: boolean;
}
function TodoList({ todos, onAdd }: {
todos: Todo[];
onAdd: (text: string) => Promise<void>;
}): JSX.Element {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(currentTodos: Todo[], newTodo: Todo) => [...currentTodos, newTodo]
);
function handleAdd(formData: FormData): void {
const text = formData.get('text') as string;
const newTodo: Todo = { id: crypto.randomUUID(), text, pending: true };
startTransition(async () => {
addOptimisticTodo(newTodo);
await onAdd(text);
});
}
return (
<div>
<form action={handleAdd}>
<input name="text" placeholder="New todo..." />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
</div>
);
}---
Pattern: Form Status in Child Component
import { useFormStatus } from 'react-dom';
import { useActionState } from 'react';
function SubmitButton({ label }: { label: string }): JSX.Element {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Processing...' : label}
</button>
);
}
function FormProgress(): JSX.Element {
const { pending, data } = useFormStatus();
if (!pending) return <></>;
const name = data?.get('name') as string | null;
return <p>Submitting {name ? `for ${name}` : ''}...</p>;
}
function RegistrationForm(): JSX.Element {
const [state, dispatch] = useActionState(registerAction, { error: null });
return (
<form action={dispatch}>
<input name="name" required />
<input name="email" type="email" required />
<FormProgress />
<SubmitButton label="Register" />
{state.error && <p className="error">{state.error}</p>}
</form>
);
}---
Pattern: Combining useActionState + useOptimistic
import { useActionState, useOptimistic, startTransition } from 'react';
interface Message {
id: string;
text: string;
sending?: boolean;
}
async function sendMessageAction(
prevState: { messages: Message[] },
formData: FormData
): Promise<{ messages: Message[] }> {
const text = formData.get('text') as string;
const saved = await saveMessage(text);
return { messages: [...prevState.messages, saved] };
}
function Chat(): JSX.Element {
const [state, dispatch, isPending] = useActionState(sendMessageAction, {
messages: [],
});
const [optimisticMessages, addOptimistic] = useOptimistic(
state.messages,
(current: Message[], newMsg: Message) => [...current, newMsg]
);
function handleSubmit(formData: FormData): void {
const text = formData.get('text') as string;
addOptimistic({ id: crypto.randomUUID(), text, sending: true });
dispatch(formData);
}
return (
<div>
<ul>
{optimisticMessages.map(msg => (
<li key={msg.id} style={{ opacity: msg.sending ? 0.5 : 1 }}>
{msg.text}
</li>
))}
</ul>
<form action={handleSubmit}>
<input name="text" disabled={isPending} />
<button type="submit" disabled={isPending}>Send</button>
</form>
</div>
);
}