Typing Component Props
Typing Component Props
Interface for Props
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
}
function Button({ label, onClick, variant = 'primary', disabled, size = 'md' }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn btn-${variant} btn-${size}`}
>
{label}
</button>
);
}
// Usage
<Button label="Click me" onClick={() => {}} variant="danger" />
Type vs Interface for Props
// Type alias for props (works the same)
type CardProps = {
title: string;
content: string;
footer?: React.ReactNode;
};
function Card({ title, content, footer }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<p>{content}</p>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
Children and Render Props
Children and Render Props
Children Prop
import { ReactNode } from 'react';
interface ContainerProps {
children: ReactNode;
title?: string;
}
function Container({ children, title }: ContainerProps) {
return (
<div className="container">
{title && <h1>{title}</h1>}
{children}
</div>
);
}
// Usage
<Container title="Dashboard">
<p>This is content inside the container.</p>
</Container>
Render Props Pattern
interface MouseTrackerProps {
render: (position: { x: number; y: number }) => ReactNode;
}
function MouseTracker({ render }: MouseTrackerProps) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{render(position)}
</div>
);
}
// Usage
<MouseTracker render={({ x, y }) => <span>Mouse at {x}, {y}</span>} />
Event Handlers
Event Handlers
Synthetic Events
import { MouseEvent, ChangeEvent, FormEvent } from 'react';
interface FormProps {
onSubmit: (data: FormData) => void;
}
function Form({ onSubmit }: FormProps) {
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit(formData);
};
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked:', e.currentTarget.textContent);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log('Input value:', e.target.value);
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button type="submit" onClick={handleClick}>Submit</button>
</form>
);
}
Generic Event Types
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
onSelect: (item: T) => void;
}
function List<T extends { id: string | number }>({ items, renderItem, onSelect }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={item.id} onClick={() => onSelect(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
Refs and Forwarding
Refs and Forwarding
useRef Typing
import { useRef } from 'react';
function TextInput() {
// DOM ref
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus</button>
</>
);
}
// Mutable ref for non-DOM values
function Stopwatch() {
const timerRef = useRef<number | null>(null);
const [time, setTime] = useState(0);
const start = () => {
timerRef.current = window.setInterval(() => {
setTime(t => t + 1);
}, 1000);
};
const stop = () => {
if (timerRef.current) {
clearInterval(timerRef.current);
}
};
return <div>{time}s</div>;
}
forwardRef
import { forwardRef } from 'react';
interface InputProps {
label: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, error, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
{error && <span className="error">{error}</span>}
</div>
)
);
Input.displayName = 'Input';