react-patterns

Modern React 19+ patterns with TypeScript

react
patterns

SKILL.md

GitHub

React Patterns

React 19+ with TypeScript.

Rules

Pattern Do Don't
Components named exports export default
Props readonly always mutable interface
Types explicit function typing React.FC
Defaults ES6 parameter defaults defaultProps
State discriminated unions bag of optionals
Errors Result types (parse/network) throw for recoverable
Critical errors throw Result type
Effects render-driven, useEffectEvent useEffect with value deps

React 19 Primitives

Hook/Component Purpose
use() extract data from promises
<Suspense> declarative loading states
useTransition loading flags, pending async
useOptimistic instant feedback during mutations
useDeferredValue stable UX during rapid updates
useEffectEvent stable callbacks without re-runs
useActionState workflow-like async state
<Activity> preserve UI state when hidden

Performance Micro-Patterns

  • Derive state during render, not in useEffect
  • Hoist static JSX outside component (avoids re-creation)

Component Structure

interface ButtonProps {
  readonly variant: "primary" | "secondary"
  readonly onClick: () => void
  readonly disabled?: boolean
  readonly userId: string | undefined  // critical fields: explicit
  readonly children?: ReactNode
}

export function Button({ variant, onClick, disabled, children }: ButtonProps) {
  return <button onClick={onClick} disabled={disabled}>{children}</button>
}

State with Discriminated Unions

type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error }

const [state, setState] = useState<AsyncState<User>>({ status: "idle" })

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} />
}

useEffectEvent

// ❌ re-runs on theme change
useEffect(() => {
  connection.on("connected", () => showNotification("Connected!", theme));
}, [roomId, theme]);

// ✅ only re-runs on roomId
const onConnected = useEffectEvent(() => showNotification("Connected!", theme));
useEffect(() => {
  connection.on("connected", () => onConnected());
}, [roomId]);

Forms (Actions)

// ✅ useActionState over onSubmit
const [state, formAction, isPending] = useActionState(updateUser, initialState);

Use useFormStatus for pending states in child components.

Result Types

type Result<T, E extends Error> = { ok: true; value: T } | { ok: false; error: E }

const result = parseJson(input)
if (!result.ok) return <Error message={result.error.message} />

Use Result for: parsing, file ops, network. Throw for: validation, critical failures.

Activity Component

<Activity mode={isSearching ? "visible" : "hidden"}>
  <SearchSpinner />
</Activity>

Preserves state when hidden — use for tabs, search overlays, conditional UI.

Anti-Patterns

// ❌ React.FC
const Button: React.FC<Props> = () => {};

// ❌ defaultProps
Button.defaultProps = { variant: "primary" };

// ❌ default exports
export default function MyComponent() {}

// ❌ mutable props
interface Props {
  count: number;
}