React Components
Quick Guide: Tiered components (Primitives -> Components -> Patterns -> Templates). React 19: pass
as a prop directly (no
needed). Expose
prop for styling flexibility. Use
for forms,
for instant feedback,
for conditional promise/context reading. Ref callbacks can return cleanup functions.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
, named constants)
(You MUST pass as a regular prop in React 19 - is deprecated)
(You MUST expose prop on ALL reusable components for customization)
(You MUST use for form submissions with pending/error state)
(You MUST call from a child component inside , NOT in the component that renders the form)
</critical_requirements>
Auto-detection: React 19, components, hooks, use(), useActionState, useFormStatus, useOptimistic, Actions, ref as prop, ref cleanup, forwardRef migration, component variants, error boundary
When to use:
- Building React components with type-safe props
- Migrating from forwardRef to React 19 ref-as-prop
- Handling form submissions with React 19 Actions API
- Creating custom hooks for reusable logic
- Implementing error boundaries with retry
When NOT to use:
- Simple one-off components without variants (skip variant abstractions)
- Static content without interactivity
Key patterns covered:
- Component architecture tiers and variant props
- React 19 ref as prop (replaces forwardRef)
- React 19 hooks: , , ,
- Ref callback cleanup functions
- Error boundaries with retry and custom fallbacks
- Custom hooks (pagination, debounce, localStorage)
- Event handler naming conventions
<philosophy>
Philosophy
React components follow a tiered architecture from low-level primitives to high-level templates. Components should be composable, type-safe, and expose necessary customization points (
, refs). Use variant abstractions only when components have multiple variant dimensions to avoid over-engineering. React is styling-agnostic -- apply styles via the
prop.
React 19 Changes: is deprecated -- pass
as a regular prop directly. New hooks (
,
,
,
) simplify data fetching and form handling with the Actions API. Ref callbacks can return cleanup functions, eliminating the need for separate
cleanup.
</philosophy>
<patterns>
Core Patterns
Pattern 1: Component Architecture Tiers
Components are organized in a tiered hierarchy:
- Primitives () - Low-level building blocks (skeleton)
- Components () - Reusable UI (button, switch, select)
- Patterns () - Composed patterns (feature, navigation)
- Templates () - Page layouts (frame)
typescript
// React 19: ref as a regular prop, no forwardRef needed
export type ButtonProps = React.ComponentProps<"button"> & {
variant?: "default" | "ghost" | "link";
size?: "default" | "large" | "icon";
asChild?: boolean;
ref?: React.Ref<HTMLButtonElement>;
};
export function Button({ variant = "default", size = "default", className, ref, ...props }: ButtonProps) {
return <button className={className} data-variant={variant} data-size={size} ref={ref} {...props} />;
}
Why good: ref as regular prop eliminates forwardRef boilerplate, className enables external styling, data-attributes enable CSS selectors for variants
See examples/core.md for complete component examples with good/bad comparisons.
Pattern 2: Component Variant Props
Components with 2+ visual dimensions (variant, size) should expose type-safe variant props via TypeScript unions. Use
attributes so any styling solution can target them.
typescript
export type AlertVariant = "info" | "warning" | "error" | "success";
export function Alert({ variant = "info", className, ref, ...props }: AlertProps) {
return <div ref={ref} className={className} data-variant={variant} {...props} />;
}
When not to use: Components with a single visual style -- skip variant abstraction.
See examples/core.md for variant props with good/bad examples.
Pattern 3: Event Handler Naming
- prefix for internal handlers: ,
- prefix for callback props: ,
- Type events explicitly:
FormEvent<HTMLFormElement>
, ChangeEvent<HTMLInputElement>
typescript
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
See examples/core.md for full event handler examples.
Pattern 4: Custom Hooks
Extract reusable logic into custom hooks following the
prefix convention.
- - Pagination state and navigation
- - Debounce values for search inputs
- - Type-safe localStorage persistence with SSR safety
See examples/hooks.md for complete implementations.
Pattern 5: Error Boundaries with Retry
Error boundaries catch render errors and provide retry capability. Place them around feature sections, not just the root.
typescript
// Key interface -- accepts custom fallback and error callback
interface Props {
children: ReactNode;
fallback?: (error: Error, reset: () => void) => ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
Limitation: Error boundaries do not catch event handler errors, async errors, or SSR errors -- use try/catch for those.
See examples/error-boundaries.md for full implementation.
Pattern 6: useActionState for Form Submissions
Skip if your framework provides its own server-side form handling (Server Actions) — use that instead.
Use
for form submissions with automatic pending state and error handling. Replaces manual
for loading/error.
typescript
import { useActionState } from "react";
async function updateProfile(prevState: string | null, formData: FormData) {
try {
await saveProfile({ name: formData.get("name") as string });
return null;
} catch {
return "Failed to save profile";
}
}
export function ProfileForm() {
const [error, submitAction, isPending] = useActionState(updateProfile, null);
return (
<form action={submitAction}>
<input type="text" name="name" disabled={isPending} />
<button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save"}
</button>
{error && <p role="alert">{error}</p>}
</form>
);
}
Why good: hook manages pending and error state automatically, form action works with progressive enhancement, no manual useState for loading/error
See examples/react-19-hooks.md for extended examples with success state.
Pattern 7: useFormStatus for Submit Buttons
reads parent form's pending state without prop drilling.
Must be called from a child component inside the
.
typescript
import { useFormStatus } from "react-dom";
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? "Submitting..." : "Submit"}
</button>
);
}
Gotcha: Calling
in the component that renders
returns
always -- it must be a descendant component.
See examples/react-19-hooks.md for reusable submit button patterns.
Pattern 8: useOptimistic for Instant UI Feedback
Show immediate UI updates while async operations complete. State automatically reverts if the request fails.
typescript
import { useOptimistic, startTransition } from "react";
const [optimisticItems, addOptimistic] = useOptimistic(
items,
(state, update: Item) => [...state, { ...update, pending: true }],
);
// In handler -- setter MUST be called inside startTransition:
startTransition(async () => {
addOptimistic(newItem);
await saveItem(newItem);
});
See examples/react-19-hooks.md for todo list and chat examples.
Pattern 9: use() Hook for Promises and Context
reads promises and context conditionally in render -- unlike
, it can be called after early returns.
typescript
import { use, Suspense } from "react";
function Comments({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise); // Suspends until resolved
return <ul>{comments.map((c) => <li key={c.id}>{c.text}</li>)}</ul>;
}
// Wrap in Suspense boundary
<Suspense fallback={<p>Loading...</p>}>
<Comments commentsPromise={fetchComments()} />
</Suspense>
Gotcha: cannot be called in try-catch blocks -- use Error Boundaries for rejected promise handling.
See examples/react-19-hooks.md for conditional context reading.
Pattern 10: Ref Callback Cleanup Functions
React 19 ref callbacks can return cleanup functions, replacing the need for separate
cleanup.
typescript
function VideoPlayer({ src }: { src: string }) {
return (
<video
ref={(video) => {
if (!video) return;
video.play();
return () => {
video.pause();
video.currentTime = 0;
};
}}
src={src}
/>
);
}
Why good: cleanup runs automatically on unmount, no separate useEffect needed, simpler than useRef + useEffect combination
Note: With ref cleanup functions, TypeScript rejects non-null/undefined return values from ref callbacks. The callback is no longer called with
on unmount -- the cleanup function handles that.
See examples/react-19-hooks.md for IntersectionObserver cleanup example.
</patterns>
Detailed Resources:
- examples/core.md - Component architecture, variants, event handlers
- examples/hooks.md - usePagination, useDebounce, useLocalStorage
- examples/react-19-hooks.md - useActionState, useFormStatus, useOptimistic, use(), ref cleanup
- examples/error-boundaries.md - Error boundary implementation and recovery
- reference.md - Decision frameworks, anti-patterns, checklists
<red_flags>
RED FLAGS
High Priority Issues:
- Using in React 19 -- deprecated, pass as a regular prop
- Calling in the component that renders -- will always return
- Using for form loading/error when exists -- unnecessary boilerplate
- Not exposing prop on reusable components -- prevents external styling
- Calling inside try-catch blocks -- use Error Boundaries instead
Medium Priority Issues:
- Adding variant abstractions for components without multiple variant dimensions
- Using on every handler regardless of child memoization (premature optimization)
- Wrapping ref callbacks in when the React Compiler handles memoization automatically
- Generic event handler names (, ) instead of descriptive names ()
Gotchas & Edge Cases:
- Ref cleanup functions: TypeScript rejects non-null/undefined returns from ref callbacks -- use the cleanup pattern explicitly
- state reverts automatically on failure -- no manual rollback needed
- can be called conditionally (after early returns), unlike
- Error boundaries do not catch event handler errors, async errors, or SSR errors
- without memoized children adds overhead without benefit
- SSR requires
typeof window !== "undefined"
before accessing browser APIs
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST pass as a regular prop in React 19 - is deprecated)
(You MUST expose prop on ALL reusable components for customization)
(You MUST use for form submissions with pending/error state)
(You MUST call from a child component inside , NOT in the component that renders the form)
Failure to follow these rules will break component composition, form state management, and styling flexibility.
</critical_reminders>