← Back to Skills Marketplace
tenequm

react-typescript

by Misha Kolesnik · GitHub ↗ · v0.1.0 · MIT-0
cross-platform ✓ Security Clean
300
Downloads
0
Stars
0
Active Installs
1
Versions
Install in OpenClaw
/install react-typescript
Description
Build React 19 applications with TypeScript. Covers Actions, Activity, use() hook, React Compiler, ref-as-prop, useEffectEvent, and strict TypeScript pattern...
README (SKILL.md)

React TypeScript

Patterns for building type-safe React 19.2 applications with TypeScript 5.9. React Compiler handles memoization automatically - write plain components, let the tooling optimize.

Critical Rules

No forwardRef - ref Is a Prop Now

// WRONG - deprecated pattern
const Input = forwardRef\x3CHTMLInputElement, InputProps>((props, ref) => (
  \x3Cinput ref={ref} {...props} />
))

// CORRECT - React 19: ref is a regular prop
function Input({ ref, ...props }: React.ComponentProps\x3C"input">) {
  return \x3Cinput ref={ref} {...props} />
}

No Manual Memoization with React Compiler

// WRONG - unnecessary with React Compiler
const MemoizedList = memo(function List({ items }: { items: Item[] }) {
  const sorted = useMemo(() => items.toSorted(compare), [items])
  const handleClick = useCallback((id: string) => onSelect(id), [onSelect])
  return sorted.map(item => \x3CRow key={item.id} onClick={() => handleClick(item.id)} />)
})

// CORRECT - React Compiler auto-memoizes all of this
function List({ items, onSelect }: { items: Item[]; onSelect: (id: string) => void }) {
  const sorted = items.toSorted(compare)
  return sorted.map(item => \x3CRow key={item.id} onClick={() => onSelect(item.id)} />)
}

Use React.ComponentProps\x3C> for Element Props

// WRONG - manual HTML attribute typing
interface ButtonProps {
  onClick?: (e: MouseEvent\x3CHTMLButtonElement>) => void
  disabled?: boolean
  children: React.ReactNode
  className?: string
}

// CORRECT - extend native element props
type ButtonProps = React.ComponentProps\x3C"button"> & {
  variant?: "primary" | "ghost"
}

Type State Discriminated Unions, Not Booleans

// WRONG - impossible states possible
interface RequestState { isLoading: boolean; error: string | null; data: User | null }

// CORRECT - discriminated union prevents impossible states
type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "error"; error: string }
  | { status: "success"; data: User }

Use satisfies for Type-Safe Literals

// WRONG - widens to Record\x3Cstring, Route>
const routes: Record\x3Cstring, Route> = { home: { path: "/" }, about: { path: "/about" } }

// CORRECT - preserves literal keys while checking shape
const routes = {
  home: { path: "/" },
  about: { path: "/about" },
} satisfies Record\x3Cstring, Route>

routes.home // typed, autocomplete works

Context Must Have Strict Defaults or Throw

// WRONG - null default with no guard
const AuthContext = createContext\x3CAuthState | null>(null)
// consumers must null-check every time

// CORRECT - factory hook that throws on missing provider
const AuthContext = createContext\x3CAuthState | null>(null)

function useAuth(): AuthState {
  const ctx = use(AuthContext)
  if (ctx === null) throw new Error("useAuth must be used within AuthProvider")
  return ctx
}

Prefer use() over useContext()

// OLD pattern
function Header() {
  const theme = useContext(ThemeContext) // cannot use after early return
  if (!isVisible) return null
  return \x3Ch1 style={{ color: theme.color }}>Title\x3C/h1>
}

// CORRECT - React 19: use() works after early returns
function Header({ isVisible }: { isVisible: boolean }) {
  if (!isVisible) return null
  const theme = use(ThemeContext) // works here - use() is not bound by hook rules
  return \x3Ch1 style={{ color: theme.color }}>Title\x3C/h1>
}

React 19 Patterns

Component Authoring

Plain functions with data-slot for styling hooks. No forwardRef, no FC:

type CardProps = React.ComponentProps\x3C"div"> & {
  variant?: "elevated" | "outlined"
}

function Card({ variant = "outlined", className, ...props }: CardProps) {
  return (
    \x3Cdiv
      data-slot="card"
      data-variant={variant}
      className={cn("rounded-xl border bg-card", className)}
      {...props}
    />
  )
}

function CardTitle({ className, ...props }: React.ComponentProps\x3C"h3">) {
  return \x3Ch3 data-slot="card-title" className={cn("font-semibold", className)} {...props} />
}

Actions and useTransition

Async functions in transitions handle pending state, errors, and form resets automatically:

function UpdateProfile({ userId }: { userId: string }) {
  const [error, submitAction, isPending] = useActionState(
    async (_prev: string | null, formData: FormData) => {
      const result = await updateProfile(userId, formData)
      if (result.error) return result.error
      redirect("/profile")
      return null
    },
    null
  )

  return (
    \x3Cform action={submitAction}>
      \x3Cinput type="text" name="displayName" required />
      \x3Cbutton type="submit" disabled={isPending}>
        {isPending ? "Saving..." : "Save"}
      \x3C/button>
      {error && \x3Cp className="text-destructive">{error}\x3C/p>}
    \x3C/form>
  )
}

useTransition for non-form Actions:

function DeleteButton({ onDelete }: { onDelete: () => Promise\x3Cvoid> }) {
  const [isPending, startTransition] = useTransition()

  return (
    \x3Cbutton
      disabled={isPending}
      onClick={() => startTransition(async () => { await onDelete() })}
    >
      {isPending ? "Deleting..." : "Delete"}
    \x3C/button>
  )
}

useOptimistic for instant feedback:

function LikeButton({ likes, onLike }: { likes: number; onLike: () => Promise\x3Cvoid> }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (prev) => prev + 1)

  const handleLike = async () => {
    addOptimisticLike(null)
    await onLike()
  }

  return (
    \x3Cform action={handleLike}>
      \x3Cbutton type="submit">{optimisticLikes} Likes\x3C/button>
    \x3C/form>
  )
}

use() Hook

Read promises and context in render. Works conditionally, after early returns:

// Reading a promise - suspends until resolved
function Comments({ commentsPromise }: { commentsPromise: Promise\x3CComment[]> }) {
  const comments = use(commentsPromise)
  return (
    \x3Cul>
      {comments.map(c => \x3Cli key={c.id}>{c.text}\x3C/li>)}
    \x3C/ul>
  )
}

// Parent gets promise from loader/cache, NOT created during render
function PostPage({ commentsPromise }: { commentsPromise: Promise\x3CComment[]> }) {
  return (
    \x3CSuspense fallback={\x3CSkeleton />}>
      \x3CComments commentsPromise={commentsPromise} />
    \x3C/Suspense>
  )
}

// Reading context conditionally
function AdminPanel({ user }: { user: User | null }) {
  if (!user) return \x3CLoginPrompt />
  const permissions = use(PermissionsContext) // legal - use() works after early return
  if (!permissions.isAdmin) return \x3CForbidden />
  return \x3CDashboard user={user} permissions={permissions} />
}

Important: use() does not support promises created during render. Pass promises from loaders, server functions, or cached sources.

Activity Component (React 19.2)

Preserve state of hidden UI. Hidden children keep their state and DOM but unmount effects:

import { Activity, useState } from "react"

function TabLayout({ tabs }: { tabs: TabConfig[] }) {
  const [activeTab, setActiveTab] = useState(tabs[0].id)

  return (
    \x3Cdiv>
      \x3Cnav>
        {tabs.map(tab => (
          \x3Cbutton key={tab.id} onClick={() => setActiveTab(tab.id)}>
            {tab.label}
          \x3C/button>
        ))}
      \x3C/nav>

      {tabs.map(tab => (
        \x3CActivity key={tab.id} mode={activeTab === tab.id ? "visible" : "hidden"}>
          \x3Ctab.component />
        \x3C/Activity>
      ))}
    \x3C/div>
  )
}

Key behaviors:

  • visible - renders normally, effects mounted
  • hidden - hides via display: none, effects cleaned up, state preserved, updates deferred
  • Pre-rendering: \x3CActivity mode="hidden"> renders children at low priority for faster future reveals
  • DOM side effects (video, audio) persist when hidden - add useLayoutEffect cleanup

useEffectEvent (React 19.2)

Extract non-reactive logic from effects. The event function always sees latest props/state without triggering effect re-runs:

function ChatRoom({ roomId, theme }: { roomId: string; theme: string }) {
  const onConnected = useEffectEvent(() => {
    showNotification("Connected!", theme) // always reads latest theme
  })

  useEffect(() => {
    const connection = createConnection(roomId)
    connection.on("connected", () => onConnected())
    connection.connect()
    return () => connection.disconnect()
  }, [roomId]) // theme NOT in deps - onConnected is an Effect Event
}

Rules:

  • Only call from inside effects or other effect events
  • Never pass to child components or include in dependency arrays
  • Never call during render
  • Use for logic that is conceptually an "event" fired from an effect

Custom hook pattern:

function useInterval(callback: () => void, delay: number | null) {
  const onTick = useEffectEvent(callback)

  useEffect(() => {
    if (delay === null) return
    const id = setInterval(() => onTick(), delay)
    return () => clearInterval(id)
  }, [delay])
}

Document Metadata

Render \x3Ctitle>, \x3Cmeta>, and \x3Clink> directly in components - React hoists them to \x3Chead>:

function BlogPost({ post }: { post: Post }) {
  return (
    \x3Carticle>
      \x3Ctitle>{post.title}\x3C/title>
      \x3Cmeta name="description" content={post.excerpt} />
      \x3Cmeta name="author" content={post.author} />
      \x3Clink rel="canonical" href={`https://example.com/posts/${post.slug}`} />
      \x3Ch1>{post.title}\x3C/h1>
      \x3Cdiv>{post.content}\x3C/div>
    \x3C/article>
  )
}

Context as Provider

const ThemeContext = createContext\x3CTheme>("light")

// React 19 - use Context directly as provider (no .Provider)
function App({ children }: { children: React.ReactNode }) {
  return (
    \x3CThemeContext value="dark">
      {children}
    \x3C/ThemeContext>
  )
}

Ref Cleanup Functions

function MeasuredBox() {
  return (
    \x3Cdiv
      ref={(node) => {
        if (node) {
          const observer = new ResizeObserver(handleResize)
          observer.observe(node)
          return () => observer.disconnect() // cleanup on unmount
        }
      }}
    />
  )
}

React Compiler

What It Does

React Compiler (babel-plugin-react-compiler) analyzes your code at build time and automatically inserts memoization. It replaces manual useMemo, useCallback, and React.memo in most cases.

Auto-memoizes:

  • Component return values (skip re-render if props unchanged)
  • Expensive computations inside components
  • Callback functions passed as props
  • JSX element creation

Setup (Vite)

pnpm add -D babel-plugin-react-compiler
// vite.config.ts
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ["babel-plugin-react-compiler"], // must be first
      },
    }),
  ],
})

What NOT to Do

// DON'T - compiler handles this
const MemoComponent = memo(MyComponent)
const memoized = useMemo(() => expensive(data), [data])
const stableCallback = useCallback(() => handler(id), [id])

// DO - write plain code, compiler optimizes
function MyComponent({ data, onSelect }: Props) {
  const processed = expensive(data)
  return \x3CChild onClick={() => onSelect(data.id)} />
}

When Manual Memoization Still Applies

  • useMemo/useCallback as effect dependencies when you need precise control over when effects fire
  • Values shared across many components (compiler memoizes per-component, not globally)
  • Opting out: "use no memo" directive skips compilation for a specific component

Verification

Components optimized by the compiler show a "Memo" badge in React DevTools. Check build output for react/compiler-runtime imports.

TypeScript Patterns

Strict tsconfig for React

{
  "compilerOptions": {
    "strict": true,
    "target": "esnext",
    "module": "nodenext",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedSideEffectImports": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "types": []
  }
}

Component Props Patterns

// Extending native element props
type ButtonProps = React.ComponentProps\x3C"button"> & {
  variant?: "primary" | "secondary"
  isLoading?: boolean
}

function Button({ variant = "primary", isLoading, children, ...props }: ButtonProps) {
  return (
    \x3Cbutton data-slot="button" disabled={isLoading || props.disabled} {...props}>
      {isLoading ? \x3CSpinner /> : children}
    \x3C/button>
  )
}

// Polymorphic "as" prop
type PolymorphicProps\x3CE extends React.ElementType> = {
  as?: E
} & Omit\x3CReact.ComponentProps\x3CE>, "as">

function Text\x3CE extends React.ElementType = "span">({
  as,
  ...props
}: PolymorphicProps\x3CE>) {
  const Component = as || "span"
  return \x3CComponent {...props} />
}

// Usage: \x3CText as="h1">Hello\x3C/Text>

Event Handler Types

function Form() {
  // Inferred from handler context - no explicit typing needed
  const handleSubmit = (e: React.FormEvent\x3CHTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)
    // process formData
  }

  const handleChange = (e: React.ChangeEvent\x3CHTMLInputElement>) => {
    console.log(e.target.value)
  }

  const handleKeyDown = (e: React.KeyboardEvent\x3CHTMLInputElement>) => {
    if (e.key === "Enter") submit()
  }

  return (
    \x3Cform onSubmit={handleSubmit}>
      \x3Cinput onChange={handleChange} onKeyDown={handleKeyDown} />
    \x3C/form>
  )
}

Hook Types

// useState - inferred when initial value provided
const [count, setCount] = useState(0) // number
const [user, setUser] = useState\x3CUser | null>(null) // explicit for null init

// useReducer - discriminated union actions
type CounterAction =
  | { type: "increment"; amount: number }
  | { type: "decrement"; amount: number }
  | { type: "reset" }

function counterReducer(state: number, action: CounterAction): number {
  switch (action.type) {
    case "increment": return state + action.amount
    case "decrement": return state - action.amount
    case "reset": return 0
  }
}

const [count, dispatch] = useReducer(counterReducer, 0)
dispatch({ type: "increment", amount: 5 })

// useRef - element refs (React 19: returns RefObject\x3CT | null>, always nullable)
const inputRef = useRef\x3CHTMLInputElement>(null)

// useRef - mutable value (no null)
const intervalRef = useRef\x3Cnumber | undefined>(undefined)

Generic Components

type SelectProps\x3CT> = {
  items: T[]
  value: T
  onChange: (item: T) => void
  getLabel: (item: T) => string
  getKey: (item: T) => string
}

function Select\x3CT>({ items, value, onChange, getLabel, getKey }: SelectProps\x3CT>) {
  return (
    \x3Cselect
      value={getKey(value)}
      onChange={(e) => {
        const item = items.find(i => getKey(i) === e.target.value)
        if (item) onChange(item)
      }}
    >
      {items.map(item => (
        \x3Coption key={getKey(item)} value={getKey(item)}>
          {getLabel(item)}
        \x3C/option>
      ))}
    \x3C/select>
  )
}

// Usage - T inferred as User
\x3CSelect
  items={users}
  value={selectedUser}
  onChange={setSelectedUser}
  getLabel={u => u.name}
  getKey={u => u.id}
/>

Discriminated Unions for Component State

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

function AsyncContent\x3CT>({
  state,
  render,
}: {
  state: AsyncState\x3CT>
  render: (data: T) => React.ReactNode
}) {
  switch (state.status) {
    case "idle": return null
    case "loading": return \x3CSpinner />
    case "error": return \x3CErrorDisplay error={state.error} />
    case "success": return \x3C>{render(state.data)}\x3C/>
  }
}

Zod v4 Integration

import { z } from "zod"

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(["admin", "user", "viewer"]),
})

type User = z.infer\x3Ctypeof UserSchema>

// Form validation with useActionState
type FieldErrors = { name?: string[]; email?: string[]; role?: string[] }

function CreateUser() {
  const [errors, submitAction, isPending] = useActionState(
    async (_prev: FieldErrors | null, formData: FormData) => {
      const result = UserSchema.safeParse(Object.fromEntries(formData))
      if (!result.success) {
        // Zod v4: use z.flattenError() to get field-level errors
        const flat = z.flattenError(result.error)
        return flat.fieldErrors as FieldErrors
      }
      await saveUser(result.data)
      return null
    },
    null
  )

  return (
    \x3Cform action={submitAction}>
      \x3Cinput name="name" />
      {errors?.name && (
        \x3Cspan className="text-destructive">{errors.name[0]}\x3C/span>
      )}
      \x3Cinput name="email" type="email" />
      \x3Cselect name="role">
        \x3Coption value="user">User\x3C/option>
        \x3Coption value="admin">Admin\x3C/option>
        \x3Coption value="viewer">Viewer\x3C/option>
      \x3C/select>
      \x3Cbutton type="submit" disabled={isPending}>Create\x3C/button>
    \x3C/form>
  )
}

Best Practices

  1. Plain functions for components - no FC, no forwardRef, no memo. FC implicitly typed children in older types and adds no value over plain function signatures. Let React Compiler optimize.
  2. React.ComponentProps\x3C"element"> for extending native elements - catches all HTML attributes.
  3. Discriminated unions over booleans for state - prevents impossible states at the type level.
  4. use() over useContext() - works conditionally, cleaner for context with guards.
  5. satisfies for config objects - preserves literal types while validating shape.
  6. Activity over conditional rendering when state preservation matters (tabs, sidebars, wizards).
  7. useEffectEvent over suppressing deps - extracts non-reactive logic cleanly from effects.
  8. Strict tsconfig - enable noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax.
  9. as const satisfies for route configs, theme tokens, and lookup objects.
  10. Type narrowing in switch - exhaustive checks via never in default cases.
  11. import defer (TS 5.9) - defer module evaluation until first property access for lazy-loaded heavy modules. See typescript-patterns.md.

Deep Dives

  • react-19-features.md - Complete React 19/19.2 feature reference with detailed examples
  • typescript-patterns.md - Advanced TypeScript patterns for React: generics, utility types, strict config, type-level programming

Resources

Usage Guidance
This skill is an offline instruction/reference for writing React 19 + TypeScript code and appears internally consistent. Because it is instruction-only, it does not request secrets or install binaries — risk is low. Consider: (1) review generated code for correctness and security before deploying, (2) note the included LICENSE.txt is Apache 2.0 if license compatibility matters, and (3) be aware the skill can be invoked autonomously by the agent (platform default), but that is not unusual or flagged here.
Capability Analysis
Type: OpenClaw Skill Name: react-typescript Version: 0.1.0 The skill bundle provides comprehensive and high-quality documentation and code patterns for React 19.2 and TypeScript 5.9 development. It covers modern features like the React Compiler, the use() hook, Activity components, and useEffectEvent. No evidence of malicious intent, data exfiltration, or harmful prompt injection was found; all instructions and code snippets are strictly aligned with the stated purpose of providing architectural guidance for type-safe React applications.
Capability Assessment
Purpose & Capability
The name/description match the content: SKILL.md and reference docs are focused on React 19 patterns and TypeScript. There are no unrelated credentials, binaries, or config paths requested.
Instruction Scope
The runtime instructions are purely coding patterns, examples, and guidance for component/hook authoring. They do not instruct the agent to read arbitrary files, environment variables, or transmit data to external endpoints outside normal developer workflows.
Install Mechanism
No install spec and no code files to execute; this is instruction-only, so nothing is downloaded or written to disk by an installer.
Credentials
The skill declares no environment variables, credentials, or config paths — proportional to an authoring-reference skill.
Persistence & Privilege
The skill is not forced-always and uses default invocation settings. It does not request persistent system presence or modify other skills/configurations.
How to Use
  1. Make sure OpenClaw is installed (local or Docker)
  2. Run the install command in chat: /install react-typescript
  3. After installation, invoke the skill by name or use /react-typescript
  4. Provide required inputs per the skill's parameter spec and get structured output
Version History
v0.1.0
Initial publish of react-typescript
Metadata
Slug react-typescript
Version 0.1.0
License MIT-0
All-time Installs 0
Active Installs 0
Total Versions 1
Frequently Asked Questions

What is react-typescript?

Build React 19 applications with TypeScript. Covers Actions, Activity, use() hook, React Compiler, ref-as-prop, useEffectEvent, and strict TypeScript pattern... It is an AI Agent Skill for Claude Code / OpenClaw, with 300 downloads so far.

How do I install react-typescript?

Run "/install react-typescript" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.

Is react-typescript free?

Yes, react-typescript is completely free, licensed under MIT-0. You can download, install and use it at no cost.

Which platforms does react-typescript support?

react-typescript is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).

Who created react-typescript?

It is built and maintained by Misha Kolesnik (@tenequm); the current version is v0.1.0.

💬 Comments