Skip to main content

Module 13: TypeScript with React

Learn how to build type-safe React applications with TypeScript, covering components, hooks, props, events, and common patterns.


1. Setup React with TypeScript​

# Create new React app with TypeScript
npx create-react-app my-app --template typescript

# Or with Vite
npm create vite@latest my-app -- --template react-ts

2. Function Components​

Basic Component​

import React from "react";

interface GreetingProps {
name: string;
age?: number;
}

const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>Age: {age}</p>}
</div>
);
};

export default Greeting;

Component Without React.FC​

interface UserCardProps {
name: string;
email: string;
onEdit: () => void;
}

function UserCard({ name, email, onEdit }: UserCardProps) {
return (
<div className="user-card">
<h2>{name}</h2>
<p>{email}</p>
<button onClick={onEdit}>Edit</button>
</div>
);
}
React.FC vs Function Declaration

Modern React recommends function declarations over React.FC as they provide better type inference for children and generics.


3. Props with Children​

interface ContainerProps {
children: React.ReactNode;
className?: string;
}

function Container({ children, className }: ContainerProps) {
return <div className={className}>{children}</div>;
}

// Usage
<Container className="main">
<h1>Title</h1>
<p>Content</p>
</Container>

Children as Function (Render Props)​

interface DataFetcherProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}

function DataFetcher<T>({ url, children }: DataFetcherProps<T>) {
const [data, setData] = React.useState<T | null>(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<Error | null>(null);

// Fetch logic...

return <>{children(data, loading, error)}</>;
}

// Usage
<DataFetcher<User> url="/api/user">
{(user, loading, error) => {
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return <p>Welcome, {user?.name}</p>;
}}
</DataFetcher>

4. Event Handlers​

interface FormProps {
onSubmit: (data: { name: string; email: string }) => void;
}

function UserForm({ onSubmit }: FormProps) {
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
onSubmit({
name: formData.get("name") as string,
email: formData.get("email") as string
});
};

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("Button clicked", event.currentTarget);
};

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log("Input changed", event.target.value);
};

return (
<form onSubmit={handleSubmit}>
<input name="name" onChange={handleChange} />
<input name="email" type="email" />
<button onClick={handleClick}>Submit</button>
</form>
);
}

Common Event Types​

React.FormEvent<HTMLFormElement>
React.MouseEvent<HTMLButtonElement>
React.ChangeEvent<HTMLInputElement>
React.KeyboardEvent<HTMLInputElement>
React.FocusEvent<HTMLInputElement>
React.ClipboardEvent<HTMLInputElement>

5. useState Hook​

import { useState } from "react";

function Counter() {
// Type inferred automatically
const [count, setCount] = useState(0);

// Explicit type
const [user, setUser] = useState<User | null>(null);

// With initial state function
const [items, setItems] = useState<string[]>(() => {
return JSON.parse(localStorage.getItem("items") || "[]");
});

return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}

6. useEffect Hook​

import { useEffect } from "react";

function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState<User | null>(null);

useEffect(() => {
let cancelled = false;

async function fetchUser() {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();

if (!cancelled) {
setUser(data);
}
}

fetchUser();

// Cleanup function
return () => {
cancelled = true;
};
}, [userId]);

return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

7. useRef Hook​

import { useRef, useEffect } from "react";

function TextInput() {
// DOM element ref
const inputRef = useRef<HTMLInputElement>(null);

// Mutable value ref
const countRef = useRef(0);

useEffect(() => {
// Focus input on mount
inputRef.current?.focus();
}, []);

const handleClick = () => {
countRef.current += 1;
console.log("Clicked", countRef.current, "times");
};

return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Click me</button>
</>
);
}

8. useContext Hook​

import { createContext, useContext, useState } from "react";

interface ThemeContextType {
theme: "light" | "dark";
toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<"light" | "dark">("light");

const toggleTheme = () => {
setTheme(prev => prev === "light" ? "dark" : "light");
};

return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}

function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error("useTheme must be used within ThemeProvider");
}
return context;
}

// Usage
function App() {
const { theme, toggleTheme } = useTheme();
return (
<div className={theme}>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}

9. useReducer Hook​

import { useReducer } from "react";

interface State {
count: number;
error: string | null;
}

type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "reset" }
| { type: "error"; payload: string };

function reducer(state: State, action: Action): State {
switch (action.type) {
case "increment":
return { ...state, count: state.count + 1, error: null };
case "decrement":
return { ...state, count: state.count - 1, error: null };
case "reset":
return { count: 0, error: null };
case "error":
return { ...state, error: action.payload };
default:
return state;
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0, error: null });

return (
<div>
<p>Count: {state.count}</p>
{state.error && <p>Error: {state.error}</p>}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}

10. Custom Hooks​

import { useState, useEffect } from "react";

interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}

function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);

const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(url);
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};

useEffect(() => {
fetchData();
}, [url]);

return { data, loading, error, refetch: fetchData };
}

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

function UserList() {
const { data, loading, error, refetch } = useFetch<User[]>("/api/users");

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

return (
<div>
<button onClick={refetch}>Refresh</button>
{data?.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}

11. Generic Components​

interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}

// Usage
interface User {
id: number;
name: string;
}

function App() {
const users: User[] = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];

return (
<List
items={users}
renderItem={user => <span>{user.name}</span>}
keyExtractor={user => user.id}
/>
);
}

12. forwardRef​

import { forwardRef } from "react";

interface InputProps {
label: string;
placeholder?: string;
}

const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, placeholder }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} placeholder={placeholder} />
</div>
);
}
);

Input.displayName = "Input";

// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null);

const handleSubmit = () => {
console.log(inputRef.current?.value);
};

return (
<div>
<Input ref={inputRef} label="Name" />
<button onClick={handleSubmit}>Submit</button>
</div>
);
}

Key Takeaways​

✅ Use interface for component props
✅ Type event handlers with React event types
✅ useState with generic for complex types
✅ Custom hooks for reusable logic
✅ useContext for global state management
✅ Generic components for flexible, reusable UI
✅ forwardRef for ref forwarding


Practice Exercises​

Exercise 1: Todo List​

interface Todo {
id: number;
text: string;
completed: boolean;
}

function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
// Implement add, toggle, delete
}

Exercise 2: Form with Validation​

Create a form component with TypeScript validation.

Exercise 3: Data Grid​

Build a generic data grid component with sorting and filtering.


Next Steps​

In Module 14, we'll explore TypeScript with Node.js, covering Express, async patterns, and building type-safe backend applications.