TypeScript with React
Building type-safe React applications with TypeScript
⚛️ What is TypeScript with React?
TypeScript enhances React development with type safety for components, props, and state. It provides better IDE support, catches errors early, and makes refactoring safer, resulting in more maintainable and robust React applications.
// Type-safe React component
interface Props {
name: string;
age: number;
}
const UserCard: React.FC<Props> = ({ name, age }) => {
return <div>{name} is {age} years old</div>;
};
Key Benefits
Type-Safe Props
Define component prop types
interface ButtonProps {
label: string;
onClick: () => void;
}
State Types
Type-safe state management
const [count, setCount] =
useState<number>(0);
Typed Hooks
Type safety for React hooks
useEffect(() => {
// Type-safe effect
}, [dependency]);
Better IntelliSense
Autocomplete for props and methods
// IDE suggests all props
<Button label="Click" />
🔹 Setting Up React with TypeScript
Create a new React TypeScript project:
# Using Create React App
npx create-react-app my-app --template typescript
# Using Vite (faster)
npm create vite@latest my-app -- --template react-ts
# Navigate to project
cd my-app
# Start development server
npm run dev
🔹 Function Components with Props
Define typed props for function components:
// Method 1: Using interface
interface GreetingProps {
name: string;
age?: number; // Optional prop
}
const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old</p>}
</div>
);
};
// Method 2: Inline props type
const Welcome = ({ message }: { message: string }) => {
return <h2>{message}</h2>;
};
// Usage
<Greeting name="John" age={25} />
<Welcome message="Welcome to React!" />
🔹 useState Hook with Types
Type-safe state management:
import { useState } from 'react';
// Simple types
const Counter = () => {
const [count, setCount] = useState<number>(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
// Complex types
interface User {
id: number;
name: string;
email: string;
}
const UserProfile = () => {
const [user, setUser] = useState<User | null>(null);
const loadUser = () => {
setUser({ id: 1, name: 'John', email: '[email protected]' });
};
return (
<div>
{user ? (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
) : (
<button onClick={loadUser}>Load User</button>
)}
</div>
);
};
🔹 Event Handlers
Type-safe event handling:
import React, { ChangeEvent, FormEvent } from 'react';
const LoginForm = () => {
const [email, setEmail] = React.useState<string>('');
const [password, setPassword] = React.useState<string>('');
// Input change handler
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
// Form submit handler
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log('Login:', { email, password });
};
// Button click handler
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked');
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={handleEmailChange}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit">Login</button>
</form>
);
};
🔹 useEffect Hook
Type-safe side effects:
import { useEffect, useState } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
const PostList = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchPosts = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data: Post[] = await response.json();
setPosts(data.slice(0, 5));
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []); // Empty dependency array
if (loading) return <div>Loading...</div>;
return (
<div>
{posts.map((post) => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
};
🔹 useRef Hook
Type-safe refs:
import { useRef, useEffect } from 'react';
const FocusInput = () => {
// Ref for input element
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// Focus input on mount
inputRef.current?.focus();
}, []);
const handleClick = () => {
// Access input value
if (inputRef.current) {
console.log('Input value:', inputRef.current.value);
}
};
return (
<div>
<input ref={inputRef} type="text" placeholder="Type here" />
<button onClick={handleClick}>Log Value</button>
</div>
);
};
// Ref for storing values
const Timer = () => {
const countRef = useRef<number>(0);
const increment = () => {
countRef.current += 1;
console.log('Count:', countRef.current);
};
return <button onClick={increment}>Increment</button>;
};
🔹 useContext Hook
Type-safe context:
import { createContext, useContext, useState, ReactNode } from 'react';
// Define context type
interface ThemeContextType {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
// Create context with default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Provider component
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook to use context
export const useTheme = () => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
};
// Usage in component
const ThemedButton = () => {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{ background: theme === 'light' ? '#fff' : '#333' }}
>
Current theme: {theme}
</button>
);
};
🔹 Custom Hooks
Create reusable type-safe hooks:
import { useState, useEffect } from 'react';
// Custom hook for fetching data
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
interface User {
id: number;
name: string;
}
const UserList = () => {
const { data, loading, error } = useFetch<User[]>('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{data?.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
};
🔹 Component Props with Children
Type children prop correctly:
import { ReactNode } from 'react';
// Method 1: Using ReactNode
interface CardProps {
title: string;
children: ReactNode;
}
const Card: React.FC<CardProps> = ({ title, children }) => {
return (
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
);
};
// Method 2: Using PropsWithChildren
import { PropsWithChildren } from 'react';
interface ContainerProps {
className?: string;
}
const Container = ({ className, children }: PropsWithChildren<ContainerProps>) => {
return <div className={className}>{children}</div>;
};
// Usage
<Card title="My Card">
<p>This is the card content</p>
</Card>
🔹 Complete Example: Todo App
Full TypeScript React application:
import { useState } from 'react';
interface Todo {
id: number;
text: string;
completed: boolean;
}
const TodoApp = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState<string>('');
const addTodo = () => {
if (input.trim()) {
const newTodo: Todo = {
id: Date.now(),
text: input,
completed: false,
};
setTodos([...todos, newTodo]);
setInput('');
}
};
const toggleTodo = (id: number) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<div>
<h1>Todo List</h1>
<div>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span
style={{
textDecoration: todo.completed ? 'line-through' : 'none',
}}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default TodoApp;