Skip to content
Back to Stack Recommender

React + TypeScript Best Practices

Type-safe React with discriminated unions, generic components, and TanStack Query for data fetching.

Full-StackModerate

Stack Components

FrameworkReact 19 + Vite
LanguageTypeScript 5.6
DataTanStack Query 5
Formsreact-hook-form + Zod
StylingTailwindCSS 4
TestingVitest + Testing Library

Why

TypeScript catches 80% of bugs at compile time. Discriminated unions make impossible states unrepresentable.

TanStack Query handles cache, retries, and background refetch — eliminating 200 lines of useEffect boilerplate per data source.

When

Choose React+TS for SPAs, design systems, and dashboards with complex state. Avoid for content sites (use Astro) or when team lacks TS experience (steep learning curve for advanced types).

How

1. Scaffold — npm create vite@latest myapp -- --template react-ts 2. Strict TS — tsconfig: strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes 3. Typed components — props interface extends HTMLAttributes, use ComponentProps<> 4. Discriminated unions — type: "loading" | "success" | "error" for state machines 5. TanStack Query — useQuery for GET, useMutation for POST, queryKey for cache 6. Forms — react-hook-form + zodResolver, handleSubmit typed end-to-end 7. Testing — Vitest + @testing-library/react, test behavior not implementation 8. Lint — eslint + typescript-eslint, strict-type-checked ruleset

Example

// Discriminated union — impossible states are unrepresentable type RequestState<T> = | { status: "idle" } | { status: "loading" } | { status: "success"; data: T } | { status: "error"; error: Error }; function UserView({ state }: { state: RequestState<User> }) { switch (state.status) { case "idle": return <p>Click to load</p>; case "loading": return <Spinner />; case "error": return <ErrorMessage error={state.error} />; case "success": return <UserCard user={state.data} />; // TS knows data exists } } // TanStack Query — typed data fetching with cache import { useQuery } from "@tanstack/react-query"; function useUser(id: string) { return useQuery({ queryKey: ["user", id], queryFn: () => fetch(`/api/users/${id}`).then((r) => r.json() as Promise<User>), staleTime: 60_000, // refetch after 1min }); } // Generic typed component function List<T>({ items, render }: { items: T[]; render: (item: T) => ReactNode }) { return <ul>{items.map((item, i) => <li key={i}>{render(item)}</li>)}</ul>; } // Usage: <List items={users} render={(u) => u.name} /> — T inferred as User

Common Problems

  • ⚠any type escapes — disable with @typescript-eslint/no-explicit-any, use unknown + narrow
  • ⚠useEffect for data fetching — causes race conditions, use TanStack Query instead
  • ⚠Prop drilling 5+ levels — use Context or pass callbacks, not intermediate props
  • ⚠Key={index} on dynamic lists — re-renders break, use stable IDs

Best Practices

  • ✓Enable strict mode + noUncheckedIndexedAccess — catches null/undefined at compile time
  • ✓Use discriminated unions for state — exhaustive switch checks prevent missing cases
  • ✓TanStack Query for all server state — cache, retry, refetch handled, no useEffect
  • ✓Test with Testing Library — queryByRole/Text, assert on accessible behavior

FAQ

What is React + TypeScript Best Practices and what projects is it for?

Type-safe React with discriminated unions, generic components, and TanStack Query for data fetching. It pairs layered technologies for end-to-end type safety and out-of-the-box deploys, fitting everything from MVPs to production SaaS.

How do I spin up a working demo with React + TypeScript Best Practices in 10 minutes?

Two steps: (1) Scaffold — npm create vite@latest myapp -- --template react-ts; (2) Strict TS — tsconfig: strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes. Get the local dev server running, then wire one real data source to verify the end-to-end loop.

What is the most common pitfall with React + TypeScript Best Practices and how do I avoid it?

The most common pitfall is: any type escapes — disable with @typescript-eslint/no-explicit-any, use unknown + narrow. Avoid it by adding the matching checklist to your PR template and running type-check + lint + unit tests in CI — three gates before merge.

What are the best practices for React + TypeScript Best Practices?

Best practices for React + TypeScript Best Practices include: Enable strict mode + noUncheckedIndexedAccess — catches null/undefined at compile time; Use discriminated unions for state — exhaustive switch checks prevent missing cases; TanStack Query for all server state — cache, retry, refetch handled, no useEffect. The first one — "Enable strict mode + noUncheckedIndexedAccess — catches null/undefined at compile time" — should be set up at project init to avoid costly rework later.

When should I NOT choose React + TypeScript Best Practices?

Pick something else in three cases: (1) the team has zero experience with the core framework and the deadline is tight (learning-curve risk); (2) it's a pure content/marketing site (Astro is lighter); (3) you need very high throughput (>10k RPS) or strict-consistency microservices (Go/Rust). React + TypeScript Best Practices shines for mid-to-high complexity full-stack and API work — don't over-engineer simple static pages with it.