Modern examples of TypeScript integration with React examples

If you’re trying to move a React codebase from plain JavaScript to TypeScript, real examples help more than any abstract type theory. This guide walks through practical, modern examples of TypeScript integration with React examples you can actually drop into a 2024-era codebase. We’ll look at typed props, hooks, context, API calls, Redux-style state, and patterns you see in production apps. Instead of toy snippets, the examples include realistic patterns: fetching JSON from an API, typing `useRef` for DOM elements, strongly typed `useReducer` actions, and integrating React Query and React Router with TypeScript. If you want the best examples of how teams are wiring TypeScript into React today, this is for you. Along the way, we’ll talk about why strict typing pays off in large React apps, how to configure your `tsconfig.json` for React, and what changed in the TypeScript + React ecosystem heading into 2024–2025. You’ll leave with copy‑pasteable patterns and a clear mental model for scaling them.
Written by
Jamie
Published

Quick, real examples of TypeScript integration with React examples

Let’s start with the kind of code you actually see in a modern React + TypeScript project. These examples of TypeScript integration with React examples assume:

  • React 18+
  • TypeScript 5+
  • JSX in .tsx files
// src/components/UserCard.tsx
import React from "react";

interface User {
  id: number;
  name: string;
  email?: string; // optional
}

interface UserCardProps {
  user: User;
  onSelect: (userId: number) => void;
}

export function UserCard({ user, onSelect }: UserCardProps) {
  return (
    <article onClick={() => onSelect(user.id)}>
      <h2>{user.name}</h2>
      {user.email && <p>{user.email}</p>}
    </article>
  );
}

That’s a simple but very real example of TypeScript integration with React: typed props, optional fields, and a callback with a clear contract. Now let’s expand into patterns you’ll hit as your app grows.


Example of typed React state and events

Early in a migration, teams usually start with typed state and event handlers. The best examples are small but highlight the value immediately.

import React, { useState, ChangeEvent, FormEvent } from "react";

interface LoginFormValues {
  email: string;
  password: string;
}

export function LoginForm() {
  const [values, setValues] = useState<LoginFormValues>({
    email: "",
    password: "",
  });

  function handleChange(
    event: ChangeEvent<HTMLInputElement>
  ) {
    const { name, value } = event.target;

    setValues(prev => ({
      ...prev,
      [name]: value,
    }));
  }

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    // values is fully typed here
    console.log(values.email, values.password);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        value={values.email}
        onChange={handleChange}
      />
      <input
        name="password"
        type="password"
        value={values.password}
        onChange={handleChange}
      />
      <button type="submit">Log in</button>
    </form>
  );
}

Here, the example of TypeScript integration with React gives you:

  • Strong typing on useState with LoginFormValues
  • Typed ChangeEvent and FormEvent so DOM APIs are discoverable in your editor

API fetching: examples include typed data and errors

Most production React apps talk to APIs. These examples of TypeScript integration with React examples show how to type both responses and errors in a way that works nicely with fetch or libraries like React Query.

// src/api/types.ts
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

export interface ApiError {
  status: number;
  message: string;
}

// src/api/client.ts
export async function fetchTodos(): Promise<Todo[]> {
  const res = await fetch("/api/todos");

  if (!res.ok) {
    const err: ApiError = {
      status: res.status,
      message: res.statusText,
    };
    throw err;
  }

  const data: Todo[] = await res.json();
  return data;
}

Then in a component:

import React, { useEffect, useState } from "react";
import { fetchTodos, Todo, ApiError } from "../api";

export function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [error, setError] = useState<ApiError | null>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let mounted = true;
    setLoading(true);

    fetchTodos()
      .then(data => {
        if (mounted) setTodos(data);
      })
      .catch((err: ApiError) => {
        if (mounted) setError(err);
      })
      .finally(() => {
        if (mounted) setLoading(false);
      });

    return () => {
      mounted = false;
    };
  }, []);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          <label>
            <input type="checkbox" checked={todo.completed} readOnly />
            {todo.title}
          </label>
        </li>
      ))}
    </ul>
  );
}

This is one of the best examples of TypeScript integration with React for teams: the types for Todo and ApiError are shared across API clients, components, and tests.

For deeper background on typed JSON handling and API contracts, it’s worth reading up on general API design guidance from organizations like NIST or web standards work via W3C, even though they’re not React‑specific.


Context API: examples of shared state with TypeScript

Shared global state is where TypeScript earns its keep. Here’s an example of a typed auth context, a very common example of TypeScript integration with React examples in production.

// src/auth/AuthContext.tsx
import React, { createContext, useContext, useState, ReactNode } from "react";

interface AuthUser {
  id: string;
  name: string;
  roles: ("admin" | "editor" | "viewer")[];
}

interface AuthContextValue {
  user: AuthUser | null;
  login: (user: AuthUser) => void;
  logout: () => void;
}

const AuthContext = createContext<AuthContextValue | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<AuthUser | null>(null);

  function login(nextUser: AuthUser) {
    setUser(nextUser);
  }

  function logout() {
    setUser(null);
  }

  const value: AuthContextValue = { user, login, logout };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

export function useAuth(): AuthContextValue {
  const ctx = useContext(AuthContext);
  if (!ctx) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return ctx;
}

This single example of TypeScript integration with React protects you from a ton of runtime bugs:

  • Consumers of useAuth always see a non‑null context value
  • roles are constrained to a literal union, which improves autocomplete and prevents typos

useReducer and discriminated unions: best examples for complex state

When state transitions get complicated, useReducer with TypeScript discriminated unions is one of the best examples of a pattern that scales.

import React, { useReducer } from "react";

interface CounterState {
  count: number;
  step: number;
}

type CounterAction =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "setStep"; payload: number };

function counterReducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case "increment":
      return { ...state, count: state.count + state.step };
    case "decrement":
      return { ...state, count: state.count - state.step };
    case "setStep":
      return { ...state, step: action.payload };
    default:
      // Exhaustiveness check
      const _never: never = action;
      return state;
  }
}

export function Counter() {
  const [state, dispatch] = useReducer(counterReducer, {
    count: 0,
    step: 1,
  });

  return (
    <section>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <input
        type="number"
        value={state.step}
        onChange={e => dispatch({ type: "setStep", payload: Number(e.target.value) })}
      />
    </section>
  );
}

This example of TypeScript integration with React gives you:

  • Type‑safe dispatch calls
  • Exhaustive switch checking, so you can’t forget a case when you add a new action type

Hooks: examples include generic custom hooks with TypeScript

Custom hooks are where TypeScript can feel intimidating, so it helps to see real examples. Here’s a generic useLocalStorage hook.

import { useEffect, useState } from "react";

export function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(() => {
    if (typeof window === "undefined") return initialValue;

    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch {
      // ignore quota errors, etc.
    }
  }, [key, value]);

  return [value, setValue] as const;
}

Usage:

const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");

This is one of those examples of TypeScript integration with React examples that shows off generics in a way that’s practical, not academic.


React Router and navigation: example of typed route params

Routing is another area where teams want type safety. With React Router v6, you can add types for route params and loader data.

// src/routes/UserDetail.tsx
import React from "react";
import { useParams } from "react-router-dom";

interface UserRouteParams {
  userId: string; // from the URL
}

export function UserDetail() {
  const { userId } = useParams<UserRouteParams>();

  return <h1>User {userId}</h1>;
}

In a larger app, you might centralize route definitions and infer types from them. That’s a more advanced example of TypeScript integration with React, but the idea is the same: let the types describe your URL structure so navigation bugs are caught at compile time.

For general guidance on building reliable web navigation and accessibility patterns, the WAI-ARIA docs at W3C are a solid reference.


React Query / data fetching libraries: real examples from 2024–2025

React Query (now TanStack Query) is everywhere in 2024–2025. Here’s how examples of TypeScript integration with React examples look when you add it to the mix.

// src/api/users.ts
export interface UserSummary {
  id: number;
  name: string;
}

export async function fetchUsers(): Promise<UserSummary[]> {
  const res = await fetch("/api/users");
  if (!res.ok) throw new Error("Failed to fetch users");
  return res.json();
}

// src/components/UserList.tsx
import React from "react";
import { useQuery } from "@tanstack/react-query";
import { fetchUsers, UserSummary } from "../api/users";

export function UserList() {
  const { data, isLoading, error } = useQuery<UserSummary[]>({
    queryKey: ["users"],
    queryFn: fetchUsers,
  });

  if (isLoading) return <p>Loading users...</p>;
  if (error instanceof Error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

This real example of TypeScript integration with React shows how generics on useQuery keep data strongly typed throughout your component tree.


useRef and DOM elements: small examples that prevent big bugs

useRef is another place where adding types pays off, especially as your UI logic gets more interactive.

import React, { useRef, useEffect } from "react";

export function AutoFocusInput() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  return <input ref={inputRef} />;
}

This tiny example of TypeScript integration with React makes the DOM API discoverable and prevents you from calling methods that don’t exist on that element type.


A few trends are shaping how new projects approach examples of TypeScript integration with React examples:

  • Stricter configs by default: New React + TS templates often enable strict, noImplicitAny, and strictNullChecks. That means your examples need to be fully typed, not halfway.
  • More server‑side React: With frameworks like Next.js and Remix, TypeScript types now span server components, loaders, and client components. Sharing types across server and client is becoming standard.
  • Stronger focus on accessibility and reliability: As apps handle more health, finance, and education data, teams care more about catching bugs early. While not React‑specific, resources like Harvard’s digital accessibility guidance can inform how you structure typed components and props.

All of that makes real, modern examples of TypeScript integration with React examples more valuable than ever. The patterns above are the ones that keep showing up in serious apps: typed API clients, context, reducers, hooks, and routing.


FAQ: common questions about examples of TypeScript integration with React

What is a simple example of adding TypeScript to an existing React component?

Rename the file from .jsx to .tsx, add an interface for props, and annotate your function. For example:

interface ButtonProps {
  label: string;
  onClick?: () => void;
}

export function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

That’s often the first example of TypeScript integration with React that teams adopt because it’s low‑risk and immediately helpful.

Do I need to type everything in my React app right away?

No. Many teams start with props and API responses, then move to context, reducers, and custom hooks. The best examples of migration are incremental: keep JavaScript where it’s still changing rapidly, and add TypeScript where contracts are relatively stable.

Are there examples of TypeScript integration with React in large open‑source projects?

Yes. Many popular libraries and frameworks maintain React + TypeScript examples, including Next.js and Remix starter templates. Browsing those repositories is a good way to see real examples of folder structure, tsconfig settings, and shared types.

How strict should my TypeScript config be for a React project?

For new projects, most teams now aim for "strict": true in tsconfig.json. That matches how modern examples of TypeScript integration with React examples are written in documentation and tutorials. For legacy apps, you can enable strictness gradually and fix errors by module or feature.


If you keep these patterns in your toolbox—typed props, state, context, reducers, hooks, routing, and data fetching—you’ll have a solid set of real‑world examples of TypeScript integration with React to apply across nearly any project.

Explore More TypeScript Code Snippets

Discover more examples and insights in this category.

View All TypeScript Code Snippets