React with TypeScript
TypeScript and React form a powerful combination for building robust, maintainable user interfaces. TypeScript provides static typing for React components, props, state, and event handlers, enabling better developer experience, fewer runtime errors, and improved code documentation.
Using TypeScript with React helps catch common mistakes like passing incorrect props, improper event handling, and state management issues at compile time rather than runtime.
Component Props and Types
Functional Components with Props
import React from 'react';
// Define props interface
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
}
// Functional component with typed props
const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'medium',
disabled = false,
onClick
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
// Usage
const App: React.FC = () => {
const handleClick = () => {
console.log('Button clicked!');
};
return (
<div>
<Button variant="primary" size="large" onClick={handleClick}>
Click me
</Button>
<Button variant="danger" disabled>
Disabled
</Button>
</div>
);
};
Component Props with Children
// Generic container component
interface ContainerProps {
children: React.ReactNode;
className?: string;
maxWidth?: number;
}
const Container: React.FC<ContainerProps> = ({ children, className, maxWidth }) => {
const style = maxWidth ? { maxWidth: `${maxWidth}px` } : undefined;
return (
<div className={`container ${className || ''}`} style={style}>
{children}
</div>
);
};
// Component with render props
interface RenderPropsComponentProps<T> {
data: T[];
renderItem: (item: T, index: number) => React.ReactNode;
loading?: boolean;
}
function List<T>({ data, renderItem, loading }: RenderPropsComponentProps<T>) {
if (loading) {
return <div>Loading...</div>;
}
return (
<ul>
{data.map((item, index) => (
<li key={index}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
const UserList: React.FC = () => {
const users: User[] = [
{ id: 1, name: 'John Doe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', email: '[email protected]' }
];
return (
<List
data={users}
renderItem={(user) => (
<div>
<strong>{user.name}</strong> - {user.email}
</div>
)}
/>
);
};
React Hooks with TypeScript
useState Hook
import React, { useState } from 'react';
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC = () => {
// Primitive state
const [count, setCount] = useState<number>(0);
const [name, setName] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(false);
// Object state with interface
const [user, setUser] = useState<User | null>(null);
// Array state
const [users, setUsers] = useState<User[]>([]);
// State with initial value (type inference)
const [message, setMessage] = useState('Initial message'); // string inferred
// Complex state with union types
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const updateUser = (updates: Partial<User>) => {
setUser(prevUser =>
prevUser ? { ...prevUser, ...updates } : null
);
};
const addUser = (newUser: User) => {
setUsers(prevUsers => [...prevUsers, newUser]);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(prev => prev + 1)}>
Increment
</button>
{user && (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
</div>
);
};
useEffect Hook
import React, { useState, useEffect } from 'react';
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
const DataFetcher: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Effect with cleanup
useEffect(() => {
let isCancelled = false;
const fetchUsers = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch('/api/users');
const result: ApiResponse<User[]> = await response.json();
if (!isCancelled) {
setUsers(result.data);
}
} catch (err) {
if (!isCancelled) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
};
fetchUsers();
// Cleanup function
return () => {
isCancelled = true;
};
}, []); // Empty dependency array
// Effect with dependencies
useEffect(() => {
const handleResize = () => {
console.log('Window resized');
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
};
Custom Hooks
// Custom hook for API calls
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const refetch = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
const result: T = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
refetch();
}, [url]);
return { data, loading, error, refetch };
}
// Custom hook for form handling
interface FormState<T> {
values: T;
errors: Partial<Record<keyof T, string>>;
touched: Partial<Record<keyof T, boolean>>;
}
function useForm<T>(initialValues: T, validate?: (values: T) => Partial<Record<keyof T, string>>) {
const [formState, setFormState] = useState<FormState<T>>({
values: initialValues,
errors: {},
touched: {}
});
const setValue = (field: keyof T, value: T[keyof T]) => {
setFormState(prev => ({
...prev,
values: { ...prev.values, [field]: value },
touched: { ...prev.touched, [field]: true },
errors: validate
? { ...prev.errors, ...validate({ ...prev.values, [field]: value }) }
: prev.errors
}));
};
const reset = () => {
setFormState({
values: initialValues,
errors: {},
touched: {}
});
};
return {
values: formState.values,
errors: formState.errors,
touched: formState.touched,
setValue,
reset
};
}
// Usage of custom hooks
const UserForm: React.FC = () => {
const { data: users, loading, refetch } = useApi<User[]>('/api/users');
const validateUser = (values: Partial<User>) => {
const errors: Partial<Record<keyof User, string>> = {};
if (!values.name) errors.name = 'Name is required';
if (!values.email) errors.email = 'Email is required';
return errors;
};
const { values, errors, touched, setValue, reset } = useForm<Partial<User>>(
{ name: '', email: '' },
validateUser
);
return (
<form>
<input
value={values.name || ''}
onChange={(e) => setValue('name', e.target.value)}
placeholder="Name"
/>
{touched.name && errors.name && <span>{errors.name}</span>}
<input
value={values.email || ''}
onChange={(e) => setValue('email', e.target.value)}
placeholder="Email"
/>
{touched.email && errors.email && <span>{errors.email}</span>}
</form>
);
};
Event Handlers
import React, { useState } from 'react';
const EventHandlerExample: React.FC = () => {
const [inputValue, setInputValue] = useState<string>('');
const [selectedFile, setSelectedFile] = useState<File | null>(null);
// Input change handler
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
};
// Form submission handler
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log('Form submitted with:', inputValue);
};
// Button click handler
const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked at:', event.clientX, event.clientY);
};
// File input handler
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] || null;
setSelectedFile(file);
};
// Keyboard event handler
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
console.log('Enter key pressed');
}
};
// Generic event handler with custom data
const handleCustomClick = (id: number) => (event: React.MouseEvent) => {
event.preventDefault();
console.log('Custom click with ID:', id);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={inputValue}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type something..."
/>
<input
type="file"
onChange={handleFileChange}
/>
<button type="button" onClick={handleButtonClick}>
Click me
</button>
<button type="button" onClick={handleCustomClick(123)}>
Custom handler
</button>
<button type="submit">Submit</button>
</form>
);
};
Context API with TypeScript
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
// State and action types
interface AppState {
user: User | null;
theme: 'light' | 'dark';
notifications: Notification[];
}
interface Notification {
id: string;
type: 'success' | 'error' | 'warning';
message: string;
}
type AppAction =
| { type: 'SET_USER'; payload: User | null }
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
| { type: 'ADD_NOTIFICATION'; payload: Notification }
| { type: 'REMOVE_NOTIFICATION'; payload: string };
// Reducer
const appReducer = (state: AppState, action: AppAction): AppState => {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'ADD_NOTIFICATION':
return {
...state,
notifications: [...state.notifications, action.payload]
};
case 'REMOVE_NOTIFICATION':
return {
...state,
notifications: state.notifications.filter(n => n.id !== action.payload)
};
default:
return state;
}
};
// Context type
interface AppContextType {
state: AppState;
dispatch: React.Dispatch<AppAction>;
// Helper functions
setUser: (user: User | null) => void;
setTheme: (theme: 'light' | 'dark') => void;
addNotification: (notification: Omit<Notification, 'id'>) => void;
removeNotification: (id: string) => void;
}
// Create context
const AppContext = createContext<AppContextType | undefined>(undefined);
// Provider component
interface AppProviderProps {
children: ReactNode;
}
export const AppProvider: React.FC<AppProviderProps> = ({ children }) => {
const initialState: AppState = {
user: null,
theme: 'light',
notifications: []
};
const [state, dispatch] = useReducer(appReducer, initialState);
// Helper functions
const setUser = (user: User | null) => {
dispatch({ type: 'SET_USER', payload: user });
};
const setTheme = (theme: 'light' | 'dark') => {
dispatch({ type: 'SET_THEME', payload: theme });
};
const addNotification = (notification: Omit<Notification, 'id'>) => {
const id = Math.random().toString(36).substr(2, 9);
dispatch({ type: 'ADD_NOTIFICATION', payload: { ...notification, id } });
};
const removeNotification = (id: string) => {
dispatch({ type: 'REMOVE_NOTIFICATION', payload: id });
};
const value: AppContextType = {
state,
dispatch,
setUser,
setTheme,
addNotification,
removeNotification
};
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
// Custom hook to use context
export const useApp = (): AppContextType => {
const context = useContext(AppContext);
if (context === undefined) {
throw new Error('useApp must be used within an AppProvider');
}
return context;
};
// Usage in components
const Header: React.FC = () => {
const { state, setTheme, setUser } = useApp();
const toggleTheme = () => {
setTheme(state.theme === 'light' ? 'dark' : 'light');
};
const logout = () => {
setUser(null);
};
return (
<header className={`header ${state.theme}`}>
<h1>My App</h1>
{state.user && <span>Welcome, {state.user.name}!</span>}
<button onClick={toggleTheme}>Toggle Theme</button>
{state.user && <button onClick={logout}>Logout</button>}
</header>
);
};
Higher-Order Components (HOCs)
import React, { ComponentType } from 'react';
// HOC that adds loading state
interface WithLoadingProps {
loading: boolean;
}
function withLoading<P extends object>(
WrappedComponent: ComponentType<P>
): ComponentType<P & WithLoadingProps> {
return (props: P & WithLoadingProps) => {
if (props.loading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...props} />;
};
}
// HOC that adds authentication
interface WithAuthProps {
user: User | null;
}
function withAuth<P extends object>(
WrappedComponent: ComponentType<P>
): ComponentType<P & WithAuthProps> {
return (props: P & WithAuthProps) => {
if (!props.user) {
return <div>Please log in to access this content.</div>;
}
return <WrappedComponent {...props} />;
};
}
// Component to be enhanced
interface ProfileProps {
userId: number;
}
const Profile: React.FC<ProfileProps> = ({ userId }) => {
return <div>Profile for user {userId}</div>;
};
// Enhanced component
const EnhancedProfile = withAuth(withLoading(Profile));
// Usage
const App: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(false);
return (
<EnhancedProfile
userId={123}
user={user}
loading={loading}
/>
);
};
Refs and forwardRef
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
// Component with ref forwarding
interface InputProps {
placeholder?: string;
onChange?: (value: string) => void;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ placeholder, onChange }, ref) => {
return (
<input
ref={ref}
placeholder={placeholder}
onChange={(e) => onChange?.(e.target.value)}
/>
);
}
);
// Component with imperative handle
interface CustomInputHandle {
focus: () => void;
clear: () => void;
getValue: () => string;
}
const CustomInput = forwardRef<CustomInputHandle, InputProps>(
({ placeholder, onChange }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current?.focus();
},
clear: () => {
if (inputRef.current) {
inputRef.current.value = '';
onChange?.('');
}
},
getValue: () => {
return inputRef.current?.value || '';
}
}));
return (
<input
ref={inputRef}
placeholder={placeholder}
onChange={(e) => onChange?.(e.target.value)}
/>
);
}
);
// Usage
const Parent: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const customInputRef = useRef<CustomInputHandle>(null);
const focusInput = () => {
inputRef.current?.focus();
};
const clearCustomInput = () => {
customInputRef.current?.clear();
};
return (
<div>
<Input ref={inputRef} placeholder="Regular input" />
<button onClick={focusInput}>Focus Input</button>
<CustomInput ref={customInputRef} placeholder="Custom input" />
<button onClick={clearCustomInput}>Clear Custom Input</button>
</div>
);
};
TypeScript with React provides excellent type safety and developer experience. The combination enables building robust applications with better error catching, improved refactoring, and self-documenting code through type definitions.