Install any skill in seconds. Free to start, no credit card required.
Get Started Free →This skill should be used when building React components with TypeScript, typing hooks, handling events, or when React TypeScript, React 19, Server Components are mentioned. Covers type-safe patterns for React 18-19 including generic components, proper event typing, and routing integration (TanStack Router, React Router).
.claude/skills/davila7-react-dev/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 101% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 68% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 146% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 125% | 0% |
Type-safe React = compile-time guarantees = confident refactoring.
<when_to_use>
NOT for: non-React TypeScript, vanilla JS React
</when_to_use>
<react_19_changes>
React 19 breaking changes require migration. Key patterns:
ref as prop - forwardRef deprecated:
typescript// React 19 - ref as regular prop type ButtonProps = { ref?: React.Ref<HTMLButtonElement>; } & React.ComponentPropsWithoutRef<'button'>; function Button({ ref, children, ...props }: ButtonProps) { return <button ref={ref} {...props}>{children}</button>; }
useActionState - replaces useFormState:
typescriptimport { useActionState } from 'react'; type FormState = { errors?: string[]; success?: boolean }; function Form() { const [state, formAction, isPending] = useActionState(submitAction, {}); return <form action={formAction}>...</form>; }
use() - unwraps promises/context:
typescriptfunction UserProfile({ userPromise }: { userPromise: Promise<User> }) { const user = use(userPromise); // Suspends until resolved return <div>{user.name}</div>; }
See react-19-patterns.md for useOptimistic, useTransition, migration checklist.
</react_19_changes>
<component_patterns>
Props - extend native elements:
typescripttype ButtonProps = { variant: 'primary' | 'secondary'; } & React.ComponentPropsWithoutRef<'button'>; function Button({ variant, children, ...props }: ButtonProps) { return <button className={variant} {...props}>{children}</button>; }
Children typing:
typescripttype Props = { children: React.ReactNode; // Anything renderable icon: React.ReactElement; // Single element render: (data: T) => React.ReactNode; // Render prop };
Discriminated unions for variant props:
typescripttype ButtonProps = | { variant: 'link'; href: string } | { variant: 'button'; onClick: () => void }; function Button(props: ButtonProps) { if (props.variant === 'link') { return <a href={props.href}>Link</a>; } return <button onClick={props.onClick}>Button</button>; }
</component_patterns>
<event_handlers>
Use specific event types for accurate target typing:
typescript// Mouse function handleClick(e: React.MouseEvent<HTMLButtonElement>) { e.currentTarget.disabled = true; } // Form function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault(); const formData = new FormData(e.currentTarget); } // Input function handleChange(e: React.ChangeEvent<HTMLInputElement>) { console.log(e.target.value); } // Keyboard function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) { if (e.key === 'Enter') e.currentTarget.blur(); }
See event-handlers.md for focus, drag, clipboard, touch, wheel events.
</event_handlers>
<hooks_typing>
useState - explicit for unions/null:
typescriptconst [user, setUser] = useState<User | null>(null); const [status, setStatus] = useState<'idle' | 'loading'>('idle');
useRef - null for DOM, value for mutable:
typescriptconst inputRef = useRef<HTMLInputElement>(null); // DOM - use ?. const countRef = useRef<number>(0); // Mutable - direct access
useReducer - discriminated unions for actions:
typescripttype Action = | { type: 'increment' } | { type: 'set'; payload: number }; function reducer(state: State, action: Action): State { switch (action.type) { case 'set': return { ...state, count: action.payload }; default: return state; } }
Custom hooks - tuple returns with as const:
typescriptfunction useToggle(initial = false) { const [value, setValue] = useState(initial); const toggle = () => setValue(v => !v); return [value, toggle] as const; }
useContext - null guard pattern:
typescriptconst UserContext = createContext<User | null>(null); function useUser() { const user = useContext(UserContext); if (!user) throw new Error('useUser outside UserProvider'); return user; }
See hooks.md for useCallback, useMemo, useImperativeHandle, useSyncExternalStore.
</hooks_typing>
<generic_components>
Generic components infer types from props - no manual annotations at call site.
Pattern - keyof T for column keys, render props for custom rendering:
typescripttype Column<T> = { key: keyof T; header: string; render?: (value: T[keyof T], item: T) => React.ReactNode; }; type TableProps<T> = { data: T[]; columns: Column<T>[]; keyExtractor: (item: T) => string | number; }; function Table<T>({ data, columns, keyExtractor }: TableProps<T>) { return ( <table> <thead> <tr>{columns.map(col => <th key={String(col.key)}>{col.header}</th>)}</tr> </thead> <tbody> {data.map(item => ( <tr key={keyExtractor(item)}> {columns.map(col => ( <td key={String(col.key)}> {col.render ? col.render(item[col.key], item) : String(item[col.key])} </td> ))} </tr> ))} </tbody> </table> ); }
Constrained generics for required properties:
typescripttype HasId = { id: string | number }; function List<T extends HasId>({ items }: { items: T[] }) { return <ul>{items.map(item => <li key={item.id}>...</li>)}</ul>; }
See generic-components.md for Select, List, Modal, FormField patterns.
</generic_components>
<server_components>
React 19 Server Components run on server, can be async.
Async data fetching:
typescriptexport default async function UserPage({ params }: { params: { id: string } }) { const user = await fetchUser(params.id); return <div>{user.name}</div>; }
Server Actions - 'use server' for mutations:
typescript'use server'; export async function updateUser(userId: string, formData: FormData) { await db.user.update({ where: { id: userId }, data: { ... } }); revalidatePath(`/users/${userId}`); }
Client + Server Action:
typescript'use client'; import { useActionState } from 'react'; import { updateUser } from '@/actions/user'; function UserForm({ userId }: { userId: string }) { const [state, formAction, isPending] = useActionState( (prev, formData) => updateUser(userId, formData), {} ); return <form action={formAction}>...</form>; }
use() for promise handoff:
typescript// Server: pass promise without await async function Page() { const userPromise = fetchUser('123'); return <UserProfile userPromise={userPromise} />; } // Client: unwrap with use() 'use client'; function UserProfile({ userPromise }: { userPromise: Promise<User> }) { const user = use(userPromise); return <div>{user.name}</div>; }
See server-components.md for parallel fetching, streaming, error boundaries.
</server_components>
<routing>
Both TanStack Router and React Router v7 provide type-safe routing solutions.
TanStack Router - Compile-time type safety with Zod validation:
typescriptimport { createRoute } from '@tanstack/react-router'; import { z } from 'zod'; const userRoute = createRoute({ path: '/users/$userId', component: UserPage, loader: async ({ params }) => ({ user: await fetchUser(params.userId) }), validateSearch: z.object({ tab: z.enum(['profile', 'settings']).optional(), page: z.number().int().positive().default(1), }), }); function UserPage() { const { user } = useLoaderData({ from: userRoute.id }); const { tab, page } = useSearch({ from: userRoute.id }); const { userId } = useParams({ from: userRoute.id }); }
React Router v7 - Automatic type generation with Framework Mode:
typescriptimport type { Route } from "./+types/user"; export async function loader({ params }: Route.LoaderArgs) { return { user: await fetchUser(params.userId) }; } export default function UserPage({ loaderData }: Route.ComponentProps) { const { user } = loaderData; // Typed from loader return <h1>{user.name}</h1>; }
See tanstack-router.md for TanStack patterns and react-router.md for React Router patterns.
</routing>
<rules>
ALWAYS:
NEVER:
</rules>
<references>
</references>
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,198 | 11,505 | -19% | 1 | 1 | 0% | 2,824 | 5,065 | +79% | 0 | 0 | — |
case-02 | fail→fail | 12,980 | 8,959 | -31% | 1 | 1 | 0% | 2,717 | 4,602 | +69% | 0 | 0 | — |
case-03 | pass→pass | 13,803 | 9,170 | -34% | 1 | 1 | 0% | 2,554 | 4,604 | +80% | 0 | 0 | — |
case-04 | fail→fail | 10,768 | 8,707 | -19% | 1 | 1 | 0% | 2,034 | 4,287 | +111% | 0 | 0 | — |
case-22 | pass→pass | 13,517 | 10,258 | -24% | 1 | 1 | 0% | 3,002 | 4,985 | +66% | 0 | 0 | — |
case-05 | pass→pass | 12,120 | 15,308 | +26% | 1 | 1 | 0% | 2,550 | 6,012 | +136% | 0 | 0 | — |
case-06 | pass→pass | 8,493 | 5,418 | -36% | 1 | 1 | 0% | 1,639 | 3,856 | +135% | 0 | 0 | — |
case-07 | pass→pass | 8,267 | 5,817 | -30% | 1 | 1 | 0% | 1,230 | 3,743 | +204% | 0 | 0 | — |
case-08 | pass→pass | 6,187 | 4,936 | -20% | 1 | 1 | 0% | 1,140 | 3,549 | +211% | 0 | 0 | — |
case-09 | pass→pass | 8,153 | 4,813 | -41% | 1 | 1 | 0% | 1,532 | 3,650 | +138% | 0 | 0 | — |
case-10 | fail→pass | 11,090 | 6,120 | -45% | 1 | 1 | 0% | 1,920 | 3,861 | +101% | 0 | 0 | — |
case-11 | fail→pass | 14,489 | 11,722 | -19% | 1 | 1 | 0% | 3,056 | 5,147 | +68% | 0 | 0 | — |
case-12 | pass→pass | 13,140 | 15,242 | +16% | 1 | 1 | 0% | 2,467 | 4,292 | +74% | 0 | 0 | — |
case-13 | pass→pass | 11,177 | 10,740 | -4% | 1 | 1 | 0% | 2,076 | 4,938 | +138% | 0 | 0 | — |
case-14 | fail→pass | 11,382 | 10,349 | -9% | 1 | 1 | 0% | 1,984 | 4,872 | +146% | 0 | 0 | — |
case-15 | pass→pass | 11,336 | 7,358 | -35% | 1 | 1 | 0% | 1,957 | 4,189 | +114% | 0 | 0 | — |
case-16 | pass→pass | 5,219 | 4,347 | -17% | 1 | 1 | 0% | 857 | 3,536 | +313% | 0 | 0 | — |
case-17 | fail→pass | 7,916 | 5,030 | -36% | 1 | 1 | 0% | 1,593 | 3,590 | +125% | 0 | 0 | — |
case-18 | fail→pass | 4,820 | 6,167 | +28% | 1 | 1 | 0% | 785 | 3,881 | +394% | 0 | 0 | — |
case-19 | fail→pass | 7,894 | 5,470 | -31% | 1 | 1 | 0% | 1,490 | 3,813 | +156% | 0 | 0 | — |
case-20 | pass→pass | 11,597 | 10,608 | -9% | 1 | 1 | 0% | 2,562 | 4,883 | +91% | 0 | 0 | — |
case-21 | pass→pass | 3,395 | 3,331 | -2% | 1 | 1 | 0% | 603 | 3,356 | +457% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.