react-patterns
Modern React 19+ patterns with TypeScript
When & Why to Use This Skill
This Claude skill provides a comprehensive framework for developing modern React 19+ applications using TypeScript. It enforces industry-leading best practices, including strict type safety, functional component patterns, and robust error handling. By prioritizing named exports, discriminated unions for state management, and the elimination of deprecated patterns like React.FC, it ensures the generation of maintainable, high-performance, and production-ready frontend code.
Use Cases
- Developing scalable React 19 applications with strict TypeScript type safety and modern component architectures.
- Refactoring legacy React codebases to align with React 19 standards and modern TypeScript conventions.
- Implementing type-safe state management using discriminated unions to eliminate illegal component states and improve reliability.
- Standardizing team coding styles by enforcing named exports, readonly props, and explicit error handling patterns.
- Optimizing component performance and maintainability by avoiding deprecated features like defaultProps and React.FC.
| name | react-patterns |
|---|---|
| description | Modern React 19+ patterns with TypeScript |
React Patterns
Purpose: Modern React 19+ with TypeScript (type safety, error handling, component best practices)
- Keywords: component, react, hook, jsx, tsx, useState, useEffect, useCallback, useMemo, ReactNode, form, FormEvent, input, validation, props, children, ref, render, conditional
Quick Reference
| Pattern | ✅ Modern (React 19+) | ❌ Avoid |
|---|---|---|
| Components | Named exports | Default exports |
| Props | readonly always |
Mutable props |
| Types | No React.FC |
React.FC<Props> |
| Props | Explicit typing | defaultProps |
| State | Discriminated unions | Bag of optionals |
| Error handling | Result types | Unchecked try/catch |
Component Structure
import type { ReactNode } from "react"
interface ButtonProps {
readonly variant: "primary" | "secondary"
readonly onClick: () => void
readonly disabled?: boolean // Safe defaults only
readonly userId: string | undefined // Critical: explicit
readonly children?: ReactNode
}
export function Button({ variant, onClick, disabled, children }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={variant === "primary" ? "btn-primary" : "btn-secondary"}
>
{children}
</button>
)
}
Key:
- Named export (not default)
- No
React.FC(outdated in React 19) - Props interface with
readonly - Explicit
undefinedfor critical fields - Optional
?only for safe defaults
State with Discriminated Unions
import { useState, useEffect } from "react"
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error }
export function UserProfile({ userId }: { readonly userId: string }) {
const [state, setState] = useState<AsyncState<User>>({ status: "idle" })
useEffect(() => {
setState({ status: "loading" })
fetchUser(userId).then(
data => setState({ status: "success", data }),
error => setState({ status: "error", error })
)
}, [userId])
switch (state.status) {
case "idle": return null
case "loading": return <Spinner />
case "success": return <UserCard user={state.data} />
case "error": return <ErrorMessage error={state.error} />
}
}
Event Handlers
export function OrderForm() {
const handleSubmit = (e: FormEvent) => {
e.preventDefault()
// ...
}
const handleAmountChange = (value: string) => { /* ... */ }
return <form onSubmit={handleSubmit}>...</form>
}
Convention: handle* prefix
Props Readonly Always
interface OrderFormProps {
readonly initialAmount: string
readonly onSubmit: (order: Order) => void
readonly onCancel?: () => void
}
export function OrderForm({ initialAmount, onSubmit }: OrderFormProps) {
return <form>...</form>
}
Result Types
type Result<T, E extends Error> =
| { ok: true; value: T }
| { ok: false; error: E }
const parseJson = (input: string): Result<unknown, Error> => {
try {
return { ok: true, value: JSON.parse(input) }
} catch (e) {
return { ok: false, error: e as Error }
}
}
// Usage
const result = parseJson(userInput)
if (!result.ok) return <Error message={result.error.message} />
// TypeScript knows result.value is safe
When to throw vs Result:
- ✅ Throw: Framework handlers, validation, critical failures
- ✅ Result: Parsing (JSON/dates), file ops, network
Check Latest Docs First
Identify framework:
next.config.js= Next.jsrouteTree.gen.ts= TanStack Startconvex/= Convex
Consult:
- React 19
- React 19.2
- TypeScript 5.9+
- Framework docs (Next.js 16, TanStack Start v1, Convex)
JSDoc When Helpful
/**
* Formats bitcoin amount with proper decimals
* @link {parseBitcoinAmount} for parsing
*/
export function formatBitcoinAmount(sats: number): string {
return (sats / 100_000_000).toFixed(8)
}
Guidelines: Only when behavior isn't self-evident, use @link for internal refs
Avoid Outdated Patterns
// ❌ React.FC (discouraged React 19)
const Button: React.FC<Props> = ({ children }) => { }
// ❌ defaultProps (deprecated React 19)
Button.defaultProps = { variant: "primary" }
// ✅ ES6 defaults
export function Button({ variant = "primary" }: Props) { }
// ❌ Default exports (except framework requirements)
export default function MyComponent() { }
// ❌ Mutable props
interface Props { count: number } // Should be readonly
Resources
resources/react-19-2-features.md- useEffectEvent, Activity, cacheSignalresources/hooks-best-practices.md- useEffect, useCallback, useMemoresources/error-boundaries.md- Error boundary patternsresources/performance.md- React.memo, useMemo, useCallback