npx skills add ...
npx skills add davila7/claude-code-templates --skill react-dev
npx skills add davila7/claude-code-templates --skill react-dev
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).
The same skill content is published under more than one repo. The install counts are split across them; any of these commands works.
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:
useActionState - replaces useFormState:
use() - unwraps promises/context:
See react-19-patterns.md for useOptimistic, useTransition, migration checklist.
</react_19_changes>
<component_patterns>
Props - extend native elements:
Children typing:
Discriminated unions for variant props:
</component_patterns>
<event_handlers>
Use specific event types for accurate target typing:
See event-handlers.md for focus, drag, clipboard, touch, wheel events.
</event_handlers>
<hooks_typing>
useState - explicit for unions/null:
useRef - null for DOM, value for mutable:
useReducer - discriminated unions for actions:
Custom hooks - tuple returns with as const:
useContext - null guard pattern:
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:
Constrained generics for required properties:
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:
Server Actions - 'use server' for mutations:
Client + Server Action:
use() for promise handoff:
See server-components.md for parallel fetching, streaming, error boundaries.
</server_components>
Both TanStack Router and React Router v7 provide type-safe routing solutions.
TanStack Router - Compile-time type safety with Zod validation:
React Router v7 - Automatic type generation with Framework Mode:
See tanstack-router.md for TanStack patterns and react-router.md for React Router patterns.
ALWAYS:
NEVER:
import { useActionState } from 'react';
type FormState = { errors?: string[]; success?: boolean };
function Form() {
const [state, formAction, isPending] = useActionState(submitAction, {});
return <form action={formAction}>...</form>;
}function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise); // Suspends until resolved
return <div>{user.name}</div>;
}type ButtonProps = {
variant: 'primary' | 'secondary';
} & React.ComponentPropsWithoutRef<'button'>;
function Button({ variant, children, ...props }: ButtonProps) {
return <button className={variant} {...props}>{children}</button>;
}type Props = {
children: React.ReactNode; // Anything renderable
icon: React.ReactElement; // Single element
render: (data: T) => React.ReactNode; // Render prop
};type 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>;
}// 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();
}const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<'idle' | 'loading'>('idle');const inputRef = useRef<HTMLInputElement>(null); // DOM - use ?.
const countRef = useRef<number>(0); // Mutable - direct accesstype 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;
}
}function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(v => !v);
return [value, toggle] as const;
}const UserContext = createContext<User | null>(null);
function useUser() {
const user = useContext(UserContext);
if (!user) throw new Error('useUser outside UserProvider');
return user;
}type 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>
);
}type HasId = { id: string | number };
function List<T extends HasId>({ items }: { items: T[] }) {
return <ul>{items.map(item => <li key={item.id}>...</li>)}</ul>;
}export default async function UserPage({ params }: { params: { id: string } }) {
const user = await fetchUser(params.id);
return <div>{user.name}</div>;
}'use server';
export async function updateUser(userId: string, formData: FormData) {
await db.user.update({ where: { id: userId }, data: { ... } });
revalidatePath(`/users/${userId}`);
}'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>;
}// 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>;
}import { 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 });
}import 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>;
}