Type-safe React = compile-time guarantees = confident refactoring. <when\_to\_use> - Building typed React components
// 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:` import { 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:` 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>; } `**Children typing**:` type Props = { children: React.ReactNode; // Anything renderable icon: React.ReactElement; // Single element render: (data: T) => React.ReactNode; // Render prop }; `**Discriminated unions** for variant props:` 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'); `**useRef** - null for DOM, value for mutable:` const inputRef = useRef<HTMLInputElement>(null); // DOM - use ?. const countRef = useRef<number>(0); // Mutable - direct access `**useReducer** - discriminated unions for actions:` type 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:` function useToggle(initial = false) { const [value, setValue] = useState(initial); const toggle = () => setValue(v => !v); return [value, toggle] as const; } `**useContext** - null guard pattern:` 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> ); } `**Constrained generics** for required properties:` 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>; } `**Server Actions** - 'use server' for mutations:` 'use server'; export async function updateUser(userId: string, formData: FormData) { await db.user.update({ where: { id: userId }, data: { ... } }); revalidatePath(`/users/${userId}`); } `**Client + Server Action**:` '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**:` // 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 }); } `**React Router v7** - Automatic type generation with Framework Mode:` 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>; }