chore: import upstream snapshot with attribution
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:58 +08:00
commit bb5c75ce05
8824 changed files with 1946442 additions and 0 deletions
@@ -0,0 +1,574 @@
# Event Handler TypeScript Patterns
Proper event typing ensures type-safe access to event properties and target elements.
## Mouse Events
```typescript
// Click events
function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
event.preventDefault();
event.stopPropagation();
// Target element typed correctly
event.currentTarget.disabled = true;
event.currentTarget.textContent = 'Clicked';
// Mouse position
console.log(event.clientX, event.clientY);
console.log(event.pageX, event.pageY);
// Mouse buttons
console.log(event.button); // 0 = left, 1 = middle, 2 = right
console.log(event.buttons); // Bitmask of pressed buttons
// Modifier keys
if (event.ctrlKey || event.metaKey) {
console.log('Ctrl/Cmd + Click');
}
if (event.shiftKey) {
console.log('Shift + Click');
}
if (event.altKey) {
console.log('Alt + Click');
}
}
// Mouse movement
function handleMouseMove(event: React.MouseEvent<HTMLDivElement>) {
const rect = event.currentTarget.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
console.log(`Position in element: ${x}, ${y}`);
}
// Hover events
function handleMouseEnter(event: React.MouseEvent<HTMLDivElement>) {
event.currentTarget.style.backgroundColor = 'lightblue';
}
function handleMouseLeave(event: React.MouseEvent<HTMLDivElement>) {
event.currentTarget.style.backgroundColor = '';
}
// Double click
function handleDoubleClick(event: React.MouseEvent<HTMLElement>) {
console.log('Double clicked');
}
```
## Form Events
```typescript
// Form submission
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);
const data = {
name: formData.get('name') as string,
email: formData.get('email') as string,
};
console.log(data);
}
// Input change
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const target = event.target;
// For text inputs
if (target.type === 'text' || target.type === 'email') {
console.log(target.value); // string
}
// For checkboxes
if (target.type === 'checkbox') {
console.log(target.checked); // boolean
}
// For radio buttons
if (target.type === 'radio') {
console.log(target.value, target.checked);
}
// For number inputs
if (target.type === 'number') {
console.log(target.valueAsNumber); // number
}
// For file inputs
if (target.type === 'file') {
const files = target.files; // FileList | null
if (files && files.length > 0) {
console.log(files[0].name);
}
}
}
// Textarea change
function handleTextareaChange(event: React.ChangeEvent<HTMLTextAreaElement>) {
console.log(event.target.value);
console.log(event.target.selectionStart); // Cursor position
}
// Select change
function handleSelectChange(event: React.ChangeEvent<HTMLSelectElement>) {
const value = event.target.value;
const selectedIndex = event.target.selectedIndex;
const selectedOption = event.target.options[selectedIndex];
console.log(value, selectedOption.text);
}
// Input events (fires on every keystroke)
function handleInput(event: React.FormEvent<HTMLInputElement>) {
console.log(event.currentTarget.value);
}
// Reset event
function handleReset(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
console.log('Form reset');
}
```
## Keyboard Events
```typescript
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
// Key identification
console.log(event.key); // 'Enter', 'Escape', 'a', etc.
console.log(event.code); // 'Enter', 'Escape', 'KeyA', etc.
// Common patterns
if (event.key === 'Enter') {
event.preventDefault();
console.log('Enter pressed');
}
if (event.key === 'Escape') {
event.currentTarget.blur();
}
// Arrow keys
if (event.key === 'ArrowUp') {
event.preventDefault();
// Navigate up
}
// Modifier keys
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
console.log('Ctrl+S - Save');
}
if (event.metaKey && event.key === 'k') {
event.preventDefault();
console.log('Cmd+K - Search');
}
// Check multiple modifiers
if (event.ctrlKey && event.shiftKey && event.key === 'P') {
event.preventDefault();
console.log('Ctrl+Shift+P - Command palette');
}
// Key combinations
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
console.log('Submit with Ctrl/Cmd+Enter');
}
}
function handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
console.log('Key released:', event.key);
}
function handleKeyPress(event: React.KeyboardEvent<HTMLInputElement>) {
// Deprecated - use keyDown instead
console.log('Key pressed:', event.key);
}
```
## Focus Events
```typescript
function handleFocus(event: React.FocusEvent<HTMLInputElement>) {
// Select all text on focus
event.target.select();
// Add visual indicator
event.currentTarget.classList.add('focused');
}
function handleBlur(event: React.FocusEvent<HTMLInputElement>) {
// Validate on blur
const value = event.target.value;
if (value === '') {
event.currentTarget.classList.add('error');
}
// Remove visual indicator
event.currentTarget.classList.remove('focused');
}
// Focus within
function handleFocusWithin(event: React.FocusEvent<HTMLDivElement>) {
// Related target - element receiving focus
const relatedTarget = event.relatedTarget as HTMLElement | null;
console.log('Focus moved from:', relatedTarget);
}
```
## Drag and Drop Events
```typescript
function handleDragStart(event: React.DragEvent<HTMLDivElement>) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', event.currentTarget.id);
// Custom drag image
const dragImage = document.createElement('div');
dragImage.textContent = 'Dragging...';
event.dataTransfer.setDragImage(dragImage, 0, 0);
}
function handleDragOver(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
event.currentTarget.classList.add('drag-over');
}
function handleDragLeave(event: React.DragEvent<HTMLDivElement>) {
event.currentTarget.classList.remove('drag-over');
}
function handleDrop(event: React.DragEvent<HTMLDivElement>) {
event.preventDefault();
event.currentTarget.classList.remove('drag-over');
const data = event.dataTransfer.getData('text/plain');
console.log('Dropped:', data);
// Handle files
const files = event.dataTransfer.files;
if (files.length > 0) {
Array.from(files).forEach((file) => {
console.log(file.name, file.type, file.size);
});
}
}
function handleDragEnd(event: React.DragEvent<HTMLDivElement>) {
console.log('Drag ended');
}
```
## Clipboard Events
```typescript
function handleCopy(event: React.ClipboardEvent<HTMLDivElement>) {
event.preventDefault();
// Custom copy behavior
const selection = window.getSelection();
if (selection) {
const text = `Copied from app: ${selection.toString()}`;
event.clipboardData.setData('text/plain', text);
}
}
function handleCut(event: React.ClipboardEvent<HTMLInputElement>) {
console.log('Cut:', event.currentTarget.value);
}
function handlePaste(event: React.ClipboardEvent<HTMLInputElement>) {
event.preventDefault();
const pastedText = event.clipboardData.getData('text/plain');
console.log('Pasted:', pastedText);
// Validate pasted content
const sanitized = pastedText.replace(/[^a-zA-Z0-9]/g, '');
event.currentTarget.value = sanitized;
}
```
## Composition Events (IME)
```typescript
// For international input methods (Chinese, Japanese, etc.)
function handleCompositionStart(event: React.CompositionEvent<HTMLInputElement>) {
console.log('Composition started');
}
function handleCompositionUpdate(event: React.CompositionEvent<HTMLInputElement>) {
console.log('Composing:', event.data);
}
function handleCompositionEnd(event: React.CompositionEvent<HTMLInputElement>) {
console.log('Composition ended:', event.data);
}
```
## Touch Events
```typescript
function handleTouchStart(event: React.TouchEvent<HTMLDivElement>) {
const touch = event.touches[0];
console.log('Touch start:', touch.clientX, touch.clientY);
}
function handleTouchMove(event: React.TouchEvent<HTMLDivElement>) {
event.preventDefault(); // Prevent scrolling
const touch = event.touches[0];
console.log('Touch move:', touch.clientX, touch.clientY);
}
function handleTouchEnd(event: React.TouchEvent<HTMLDivElement>) {
console.log('Touch ended');
}
// Multi-touch
function handleMultiTouch(event: React.TouchEvent<HTMLDivElement>) {
if (event.touches.length === 2) {
const [touch1, touch2] = event.touches;
const distance = Math.hypot(
touch2.clientX - touch1.clientX,
touch2.clientY - touch1.clientY
);
console.log('Pinch distance:', distance);
}
}
```
## Wheel Events
```typescript
function handleWheel(event: React.WheelEvent<HTMLDivElement>) {
// Prevent default scroll
event.preventDefault();
// Scroll delta
console.log('Delta X:', event.deltaX);
console.log('Delta Y:', event.deltaY);
console.log('Delta Z:', event.deltaZ);
// Delta mode (0 = pixels, 1 = lines, 2 = pages)
console.log('Delta mode:', event.deltaMode);
// Zoom with Ctrl+Wheel
if (event.ctrlKey) {
const zoomDelta = event.deltaY > 0 ? -0.1 : 0.1;
console.log('Zoom:', zoomDelta);
}
}
```
## Generic Event Handlers
Reusable handlers with proper typing.
```typescript
// Generic change handler
function createChangeHandler<T extends HTMLElement>(
callback: (value: string) => void
) {
return (event: React.ChangeEvent<T>) => {
if ('value' in event.target) {
callback(event.target.value);
}
};
}
// Usage
const handleNameChange = createChangeHandler<HTMLInputElement>((value) => {
setName(value);
});
const handleBioChange = createChangeHandler<HTMLTextAreaElement>((value) => {
setBio(value);
});
// Generic click handler with target validation
function createClickHandler<T extends HTMLElement>(
selector: string,
callback: (element: T) => void
) {
return (event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement;
const match = target.closest(selector);
if (match) {
callback(match as T);
}
};
}
// Usage
const handleItemClick = createClickHandler<HTMLLIElement>('li[data-id]', (item) => {
const id = item.dataset.id;
console.log('Clicked item:', id);
});
```
## Event Handler Type Aliases
```typescript
// Create reusable type aliases
type InputChangeHandler = React.ChangeEventHandler<HTMLInputElement>;
type ButtonClickHandler = React.MouseEventHandler<HTMLButtonElement>;
type FormSubmitHandler = React.FormEventHandler<HTMLFormElement>;
// Usage
const handleChange: InputChangeHandler = (event) => {
console.log(event.target.value);
};
const handleClick: ButtonClickHandler = (event) => {
event.currentTarget.disabled = true;
};
const handleSubmit: FormSubmitHandler = (event) => {
event.preventDefault();
};
// Generic handler type
type EventHandler<E extends HTMLElement, Evt extends React.SyntheticEvent> = (
event: Evt & { currentTarget: E }
) => void;
// Usage
const handleInput: EventHandler<HTMLInputElement, React.ChangeEvent<HTMLInputElement>> = (
event
) => {
console.log(event.currentTarget.value);
};
```
## Delegated Event Handlers
Type-safe event delegation.
```typescript
function ListContainer() {
const handleListClick = (event: React.MouseEvent<HTMLUListElement>) => {
const target = event.target as HTMLElement;
// Find clicked list item
const listItem = target.closest('li');
if (!listItem) return;
// Type guard for data attributes
const itemId = listItem.getAttribute('data-id');
if (itemId) {
console.log('Clicked item:', itemId);
}
// Handle button within list item
if (target.matches('button.delete')) {
event.stopPropagation();
console.log('Delete clicked');
}
};
return (
<ul onClick={handleListClick}>
<li data-id="1">
Item 1<button className="delete">Delete</button>
</li>
<li data-id="2">
Item 2<button className="delete">Delete</button>
</li>
</ul>
);
}
```
## Native vs Synthetic Events
```typescript
function Component() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
// Native event listener
const handleNativeClick = (event: MouseEvent) => {
console.log('Native click:', event.target);
};
element.addEventListener('click', handleNativeClick);
return () => {
element.removeEventListener('click', handleNativeClick);
};
}, []);
// React synthetic event
const handleReactClick = (event: React.MouseEvent<HTMLDivElement>) => {
console.log('React click:', event.target);
// Access native event
const nativeEvent = event.nativeEvent;
console.log('Native event:', nativeEvent);
};
return <div ref={ref} onClick={handleReactClick}>Click me</div>;
}
```
## Custom Events
```typescript
// Define custom event type
type CustomEventMap = {
'user:login': CustomEvent<{ userId: string }>;
'user:logout': CustomEvent;
};
// Typed custom event dispatcher
function dispatchCustomEvent<K extends keyof CustomEventMap>(
element: HTMLElement,
type: K,
detail?: CustomEventMap[K]['detail']
) {
const event = new CustomEvent(type, { detail, bubbles: true });
element.dispatchEvent(event);
}
// Component using custom events
function UserButton() {
const ref = useRef<HTMLButtonElement>(null);
const handleLogin = () => {
if (ref.current) {
dispatchCustomEvent(ref.current, 'user:login', { userId: '123' });
}
};
useEffect(() => {
const element = ref.current;
if (!element) return;
const handleCustomLogin = (event: Event) => {
const customEvent = event as CustomEvent<{ userId: string }>;
console.log('User logged in:', customEvent.detail.userId);
};
element.addEventListener('user:login', handleCustomLogin);
return () => {
element.removeEventListener('user:login', handleCustomLogin);
};
}, []);
return <button ref={ref} onClick={handleLogin}>Login</button>;
}
```
@@ -0,0 +1,456 @@
# Hook TypeScript Patterns
Type-safe hook patterns for useState, useRef, useReducer, useContext, and custom hooks.
## useState
Type inference works for simple types; explicit typing needed for unions/null.
```typescript
// Inference works
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [items, setItems] = useState<string[]>([]); // explicit for empty arrays
// Explicit for unions/null
const [user, setUser] = useState<User | null>(null);
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle');
// Complex initial state
type FormData = { name: string; email: string };
const [formData, setFormData] = useState<FormData>({
name: '',
email: '',
});
// Lazy initialization
const [data, setData] = useState<Data>(() => {
const cached = localStorage.getItem('data');
return cached ? JSON.parse(cached) : defaultData;
});
```
## useRef
Distinguish DOM refs (null initial) from mutable value refs (value initial).
```typescript
// DOM element ref - null initial, readonly .current
const inputRef = useRef<HTMLInputElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const divRef = useRef<HTMLDivElement>(null);
useEffect(() => {
inputRef.current?.focus(); // Optional chaining for null
}, []);
// Mutable value ref - non-null initial, mutable .current
const countRef = useRef<number>(0);
countRef.current += 1; // No optional chaining
const previousValueRef = useRef<string | undefined>(undefined);
previousValueRef.current = currentValue;
// Interval/timeout ref
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const intervalRef = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
timeoutRef.current = setTimeout(() => {}, 1000);
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
// Callback ref for dynamic elements
const callbackRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
node.scrollIntoView({ behavior: 'smooth' });
}
}, []);
```
## useReducer
Typed actions with discriminated unions.
```typescript
type State = {
count: number;
status: 'idle' | 'loading' | 'success' | 'error';
error?: string;
};
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; payload: number }
| { type: 'setStatus'; payload: State['status'] }
| { type: 'setError'; payload: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + 1 };
case 'decrement':
return { ...state, count: state.count - 1 };
case 'set':
return { ...state, count: action.payload };
case 'setStatus':
return { ...state, status: action.payload };
case 'setError':
return { ...state, status: 'error', error: action.payload };
default:
return state;
}
}
function Component() {
const [state, dispatch] = useReducer(reducer, {
count: 0,
status: 'idle',
});
dispatch({ type: 'set', payload: 10 }); // Type-safe
dispatch({ type: 'set' }); // Error: payload required
dispatch({ type: 'unknown' }); // Error: invalid action type
}
```
### Reducer with Context
```typescript
type AuthState = {
user: User | null;
isAuthenticated: boolean;
};
type AuthAction =
| { type: 'login'; payload: User }
| { type: 'logout' };
type AuthContextValue = {
state: AuthState;
dispatch: React.Dispatch<AuthAction>;
};
const AuthContext = createContext<AuthContextValue | null>(null);
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case 'login':
return { user: action.payload, isAuthenticated: true };
case 'logout':
return { user: null, isAuthenticated: false };
}
}
function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(authReducer, {
user: null,
isAuthenticated: false,
});
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
```
## useContext
Typed context with and without default values.
```typescript
// Context with default value
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
function useTheme() {
return useContext(ThemeContext); // Always Theme, never null
}
// Context without default (must handle null)
type User = { id: string; name: string };
const UserContext = createContext<User | null>(null);
function useUser() {
const user = useContext(UserContext);
if (!user) throw new Error('useUser must be used within UserProvider');
return user; // Type narrowed to User
}
// Context with complex value
type AppContextValue = {
theme: Theme;
user: User | null;
setTheme: (theme: Theme) => void;
login: (user: User) => void;
logout: () => void;
};
const AppContext = createContext<AppContextValue | null>(null);
function useApp() {
const context = useContext(AppContext);
if (!context) throw new Error('useApp must be used within AppProvider');
return context;
}
```
## Custom Hooks
Return type patterns for simple and complex hooks.
```typescript
// Object return - properties accessed by name
function useCounter(initial: number) {
const [count, setCount] = useState(initial);
const increment = () => setCount((c) => c + 1);
const decrement = () => setCount((c) => c - 1);
const reset = () => setCount(initial);
return { count, increment, decrement, reset };
}
// Usage
const { count, increment } = useCounter(0);
// Tuple return - positional destructuring
function useToggle(initial = false): [boolean, () => void, () => void, () => void] {
const [value, setValue] = useState(initial);
const toggle = () => setValue((v) => !v);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, toggle, setTrue, setFalse];
}
// Usage
const [isOpen, toggleOpen, open, close] = useToggle();
// as const for tuple inference
function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const; // readonly tuple
}
// Generic custom hook
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
fetch(url)
.then((res) => res.json())
.then((json: T) => {
if (!cancelled) {
setData(json);
setLoading(false);
}
})
.catch((err) => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage - T inferred from usage or explicit
const { data } = useFetch<User[]>('/api/users');
```
## useCallback and useMemo
Typed callbacks and memoized values.
```typescript
// useCallback with typed parameters
const handleClick = useCallback((id: string, event: React.MouseEvent) => {
console.log(id, event.target);
}, []);
// useCallback returning value
const formatDate = useCallback((date: Date): string => {
return date.toLocaleDateString();
}, []);
// useMemo with explicit return type
const sortedItems = useMemo((): Item[] => {
return [...items].sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
// useMemo with complex computation
const stats = useMemo(() => {
return {
total: items.length,
average: items.reduce((a, b) => a + b.value, 0) / items.length,
max: Math.max(...items.map((i) => i.value)),
};
}, [items]);
```
## useImperativeHandle
Expose imperative methods from components.
```typescript
type InputHandle = {
focus: () => void;
clear: () => void;
getValue: () => string;
};
type InputProps = {
ref?: React.Ref<InputHandle>;
label: string;
};
function CustomInput({ ref, label }: InputProps) {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => {
if (inputRef.current) inputRef.current.value = '';
},
getValue: () => inputRef.current?.value ?? '',
}));
return (
<div>
<label>{label}</label>
<input ref={inputRef} />
</div>
);
}
// Usage
function Form() {
const inputRef = useRef<InputHandle>(null);
const handleSubmit = () => {
const value = inputRef.current?.getValue();
inputRef.current?.clear();
};
return (
<form>
<CustomInput ref={inputRef} label="Name" />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</form>
);
}
```
## useLayoutEffect
Same signature as useEffect, runs synchronously after DOM mutations.
```typescript
function Tooltip({ targetRef }: { targetRef: React.RefObject<HTMLElement> }) {
const tooltipRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ top: 0, left: 0 });
useLayoutEffect(() => {
if (targetRef.current && tooltipRef.current) {
const rect = targetRef.current.getBoundingClientRect();
setPosition({
top: rect.bottom + 8,
left: rect.left,
});
}
}, [targetRef]);
return (
<div
ref={tooltipRef}
style={{ position: 'fixed', top: position.top, left: position.left }}
>
Tooltip content
</div>
);
}
```
## useId
Generate unique IDs for accessibility.
```typescript
function FormField({ label }: { label: string }) {
const id = useId();
const errorId = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} aria-describedby={errorId} />
<span id={errorId}>Error message</span>
</div>
);
}
```
## useSyncExternalStore
Subscribe to external stores with SSR support.
```typescript
type Store<T> = {
getState: () => T;
subscribe: (callback: () => void) => () => void;
};
function useStore<T>(store: Store<T>): T {
return useSyncExternalStore(
store.subscribe,
store.getState,
store.getState // Server snapshot
);
}
// Example: window width store
const widthStore: Store<number> = {
getState: () => (typeof window !== 'undefined' ? window.innerWidth : 0),
subscribe: (callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
},
};
function useWindowWidth() {
return useSyncExternalStore(
widthStore.subscribe,
widthStore.getState,
() => 0 // Server fallback
);
}
```
@@ -0,0 +1,638 @@
# React 19 TypeScript Patterns
React 19 introduces breaking changes and new APIs requiring updated TypeScript patterns.
## ref as Prop (No More forwardRef)
React 19 allows ref as regular prop — forwardRef deprecated but still works.
```typescript
// ✅ React 19 - ref as prop
type InputProps = {
ref?: React.Ref<HTMLInputElement>;
label: string;
} & React.ComponentPropsWithoutRef<'input'>;
export function Input({ ref, label, ...props }: InputProps) {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
}
// Usage
function Form() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<form>
<Input ref={inputRef} label="Name" />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</form>
);
}
```
```typescript
// ❌ Old pattern (still works, but unnecessary)
import { forwardRef } from 'react';
type InputProps = {
label: string;
} & React.ComponentPropsWithoutRef<'input'>;
export const Input = forwardRef<HTMLInputElement, InputProps>(
({ label, ...props }, ref) => {
return (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
);
}
);
Input.displayName = 'Input';
```
### Generic Components with ref
```typescript
type SelectProps<T> = {
ref?: React.Ref<HTMLSelectElement>;
options: T[];
value: T;
onChange: (value: T) => void;
getLabel: (option: T) => string;
};
export function Select<T>({ ref, options, value, onChange, getLabel }: SelectProps<T>) {
return (
<select
ref={ref}
value={getLabel(value)}
onChange={(e) => {
const selected = options.find((opt) => getLabel(opt) === e.target.value);
if (selected) onChange(selected);
}}
>
{options.map((opt) => (
<option key={getLabel(opt)} value={getLabel(opt)}>
{getLabel(opt)}
</option>
))}
</select>
);
}
```
### Combining ref with Other Props
```typescript
type ButtonProps = {
ref?: React.Ref<HTMLButtonElement>;
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
} & React.ComponentPropsWithoutRef<'button'>;
export function Button({
ref,
variant = 'primary',
size = 'md',
className,
children,
...props
}: ButtonProps) {
return (
<button
ref={ref}
className={`btn btn-${variant} btn-${size} ${className || ''}`}
{...props}
>
{children}
</button>
);
}
```
## useActionState - Form State Management
Replaces useFormState — manages form submission state with Server Actions.
```typescript
'use client';
import { useActionState } from 'react';
type FormState = {
success?: boolean;
errors?: Record<string, string[]>;
message?: string;
};
type FormData = {
email: string;
password: string;
};
// Server Action
async function login(
prevState: FormState,
formData: FormData
): Promise<FormState> {
'use server';
const email = formData.get('email');
const password = formData.get('password');
if (!email || typeof email !== 'string') {
return {
success: false,
errors: { email: ['Email is required'] },
};
}
if (!password || typeof password !== 'string') {
return {
success: false,
errors: { password: ['Password is required'] },
};
}
try {
await signIn(email, password);
return { success: true, message: 'Logged in successfully' };
} catch (error) {
return {
success: false,
message: 'Invalid credentials',
};
}
}
// Client Component
export function LoginForm() {
const [state, formAction, isPending] = useActionState<FormState, FormData>(
login,
{} // Initial state
);
return (
<form action={formAction}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
required
aria-invalid={!!state.errors?.email}
/>
{state.errors?.email?.map((error) => (
<p key={error} className="error">{error}</p>
))}
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
name="password"
type="password"
required
aria-invalid={!!state.errors?.password}
/>
{state.errors?.password?.map((error) => (
<p key={error} className="error">{error}</p>
))}
</div>
{state.message && (
<div className={state.success ? 'success' : 'error'}>
{state.message}
</div>
)}
<button type="submit" disabled={isPending}>
{isPending ? 'Logging in...' : 'Log In'}
</button>
</form>
);
}
```
### useActionState with Optimistic Updates
```typescript
'use client';
import { useActionState, useOptimistic } from 'react';
type Todo = { id: string; title: string; completed: boolean };
async function toggleTodo(
prevState: { todos: Todo[] },
formData: FormData
): Promise<{ todos: Todo[] }> {
'use server';
const todoId = formData.get('todoId') as string;
await db.todo.update({
where: { id: todoId },
data: { completed: { toggle: true } },
});
const todos = await db.todo.findMany();
return { todos };
}
export function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [state, formAction] = useActionState(
toggleTodo,
{ todos: initialTodos }
);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(
state.todos,
(currentTodos, todoId: string) =>
currentTodos.map((todo) =>
todo.id === todoId ? { ...todo, completed: !todo.completed } : todo
)
);
return (
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id}>
<form
action={(formData) => {
setOptimisticTodos(todo.id);
formAction(formData);
}}
>
<input type="hidden" name="todoId" value={todo.id} />
<button type="submit">
{todo.completed ? '✓' : '○'} {todo.title}
</button>
</form>
</li>
))}
</ul>
);
}
```
## use() Hook - Unwrapping Resources
use() unwraps promises and context — enables new patterns for data fetching.
### use() with Promises
```typescript
// Server Component
async function UserPage({ params }: { params: { id: string } }) {
// Pass promise without awaiting
const userPromise = fetchUser(params.id);
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
);
}
// Client Component
'use client';
import { use } from 'react';
type UserProfileProps = {
userPromise: Promise<User>;
};
export function UserProfile({ userPromise }: UserProfileProps) {
// Suspends until resolved
const user = use(userPromise);
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
```
### Conditional use()
use() can be called conditionally — unlike hooks.
```typescript
'use client';
import { use } from 'react';
type Props = {
userPromise?: Promise<User>;
userId?: string;
};
export function UserDisplay({ userPromise, userId }: Props) {
let user: User | undefined;
if (userPromise) {
user = use(userPromise); // Conditional use() - allowed!
} else if (userId) {
// Fetch inline
user = use(fetchUser(userId));
}
if (!user) return <div>No user data</div>;
return <div>{user.name}</div>;
}
```
### use() in Loops
```typescript
'use client';
import { use } from 'react';
type Props = {
userPromises: Promise<User>[];
};
export function UserList({ userPromises }: Props) {
return (
<ul>
{userPromises.map((promise, index) => {
const user = use(promise); // use() in loop - allowed!
return <li key={user.id}>{user.name}</li>;
})}
</ul>
);
}
```
### use() with Context
Alternative to useContext — can be called conditionally.
```typescript
import { createContext, use } from 'react';
type Theme = 'light' | 'dark';
const ThemeContext = createContext<Theme>('light');
export function ThemedComponent({ override }: { override?: Theme }) {
let theme: Theme;
if (override) {
theme = override;
} else {
theme = use(ThemeContext); // Conditional context access
}
return <div className={theme}>Content</div>;
}
```
## useOptimistic - Optimistic UI Updates
Show immediate UI feedback before server confirms.
```typescript
'use client';
import { useOptimistic } from 'react';
type Message = { id: string; text: string; sending?: boolean };
export function MessageThread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage: Message) => [...state, newMessage]
);
async function sendMessage(formData: FormData) {
const text = formData.get('message') as string;
// Add optimistic message immediately
addOptimisticMessage({ id: 'temp', text, sending: true });
// Send to server
await fetch('/api/messages', {
method: 'POST',
body: JSON.stringify({ text }),
});
}
return (
<div>
<ul>
{optimisticMessages.map((msg) => (
<li key={msg.id} className={msg.sending ? 'opacity-50' : ''}>
{msg.text}
</li>
))}
</ul>
<form action={sendMessage}>
<input name="message" required />
<button type="submit">Send</button>
</form>
</div>
);
}
```
### useOptimistic with State Updates
```typescript
'use client';
import { useOptimistic, useState, useTransition } from 'react';
type Item = { id: string; name: string; quantity: number };
export function ShoppingCart({ items: initialItems }: { items: Item[] }) {
const [items, setItems] = useState(initialItems);
const [isPending, startTransition] = useTransition();
const [optimisticItems, updateOptimistic] = useOptimistic(
items,
(state, { id, quantity }: { id: string; quantity: number }) =>
state.map((item) =>
item.id === id ? { ...item, quantity } : item
)
);
async function updateQuantity(id: string, quantity: number) {
// Optimistic update
updateOptimistic({ id, quantity });
// Server update
startTransition(async () => {
const updated = await fetch(`/api/cart/${id}`, {
method: 'PATCH',
body: JSON.stringify({ quantity }),
}).then((r) => r.json());
setItems(updated);
});
}
return (
<ul>
{optimisticItems.map((item) => (
<li key={item.id}>
{item.name}
<button onClick={() => updateQuantity(item.id, item.quantity - 1)}>
-
</button>
<span>{item.quantity}</span>
<button onClick={() => updateQuantity(item.id, item.quantity + 1)}>
+
</button>
</li>
))}
</ul>
);
}
```
## useTransition - Non-Blocking Updates
Mark state updates as non-urgent — UI stays responsive.
```typescript
'use client';
import { useTransition, useState } from 'react';
export function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<string[]>([]);
const [isPending, startTransition] = useTransition();
function handleSearch(value: string) {
setQuery(value); // Urgent - update input immediately
startTransition(() => {
// Non-urgent - can be interrupted
const filtered = hugeDataset.filter((item) =>
item.toLowerCase().includes(value.toLowerCase())
);
setResults(filtered);
});
}
return (
<div>
<input
type="text"
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
{isPending && <div>Searching...</div>}
<ul>
{results.map((result) => (
<li key={result}>{result}</li>
))}
</ul>
</div>
);
}
```
### useTransition with Server Actions
```typescript
'use client';
import { useTransition } from 'react';
import { deletePost } from '@/actions/posts';
export function DeleteButton({ postId }: { postId: string }) {
const [isPending, startTransition] = useTransition();
function handleDelete() {
startTransition(async () => {
await deletePost(postId);
// UI stays responsive during deletion
});
}
return (
<button onClick={handleDelete} disabled={isPending}>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}
```
## useDeferredValue - Deferred Rendering
Defer expensive re-renders while keeping UI responsive.
```typescript
'use client';
import { useDeferredValue, useState } from 'react';
export function ProductSearch() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{/* Uses deferred value - won't block input */}
<ExpensiveResults query={deferredQuery} />
</div>
);
}
function ExpensiveResults({ query }: { query: string }) {
const results = useMemo(() => {
// Expensive filtering/sorting
return products.filter((p) => p.name.includes(query));
}, [query]);
return (
<ul>
{results.map((result) => (
<li key={result.id}>{result.name}</li>
))}
</ul>
);
}
```
## Migration Checklist
Updating from React 18 to React 19:
- [ ] Replace forwardRef with ref as prop
- [ ] Replace useFormState with useActionState
- [ ] Update Server Action types to include prevState parameter
- [ ] Use use() for promises in Server Components
- [ ] Add 'use server' directive to Server Actions
- [ ] Add 'use client' directive to Client Components
- [ ] Update TypeScript to 5.0+ for better React 19 support
- [ ] Update @types/react to 19.x
- [ ] Test all forms with useActionState
- [ ] Verify ref forwarding works without forwardRef
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,587 @@
# TanStack Router TypeScript Patterns
TanStack Router provides full type safety for routes, params, search params, and loader data.
## Basic Route Definition
```typescript
import { createRoute, createRootRoute } from '@tanstack/react-router';
// Root route
const rootRoute = createRootRoute({
component: RootLayout,
});
// Basic route
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: HomePage,
});
// Route tree
const routeTree = rootRoute.addChildren([indexRoute]);
// Router
const router = createRouter({ routeTree });
// Type for router - use in app
type Router = typeof router;
```
## Route with Params
```typescript
import { createRoute } from '@tanstack/react-router';
import { useParams } from '@tanstack/react-router';
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
});
function UserPage() {
const { userId } = useParams({ from: userRoute.id });
// userId is typed as string
return <div>User ID: {userId}</div>;
}
// Multiple params
const postRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId/posts/$postId',
component: PostPage,
});
function PostPage() {
const { userId, postId } = useParams({ from: postRoute.id });
// Both typed as string
return <div>Post {postId} by user {userId}</div>;
}
```
## Search Params with Validation
```typescript
import { z } from 'zod';
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
component: ProductsPage,
validateSearch: z.object({
category: z.string().optional(),
sortBy: z.enum(['price', 'name', 'rating']).default('name'),
page: z.number().int().positive().default(1),
perPage: z.number().int().positive().default(20),
}),
});
function ProductsPage() {
const search = useSearch({ from: productsRoute.id });
// search is typed from Zod schema:
// {
// category?: string;
// sortBy: 'price' | 'name' | 'rating';
// page: number;
// perPage: number;
// }
return (
<div>
Category: {search.category || 'All'}
Sort by: {search.sortBy}
Page: {search.page}
</div>
);
}
```
## Loader Data
```typescript
type User = {
id: string;
name: string;
email: string;
};
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
function UserPage() {
const { user } = useLoaderData({ from: userRoute.id });
// user typed as User
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
```
## Loader with Search Params
```typescript
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
component: ProductsPage,
validateSearch: z.object({
category: z.string().optional(),
page: z.number().default(1),
}),
loader: async ({ search }) => {
// search is typed from validateSearch
const products = await fetchProducts({
category: search.category,
page: search.page,
});
return { products };
},
});
function ProductsPage() {
const { products } = useLoaderData({ from: productsRoute.id });
const search = useSearch({ from: productsRoute.id });
return (
<div>
<h1>Products - {search.category || 'All'}</h1>
<ul>
{products.map((p) => (
<li key={p.id}>{p.name}</li>
))}
</ul>
</div>
);
}
```
## Type-Safe Navigation
```typescript
import { useNavigate, Link } from '@tanstack/react-router';
function ProductList() {
const navigate = useNavigate();
const goToProduct = (productId: string) => {
navigate({
to: '/products/$productId',
params: { productId }, // Type-checked
search: { tab: 'reviews' }, // Type-checked against validateSearch
});
};
return (
<div>
{products.map((product) => (
<Link
key={product.id}
to="/products/$productId"
params={{ productId: product.id }}
search={{ tab: 'details' }}
>
{product.name}
</Link>
))}
</div>
);
}
```
## Search Params Manipulation
```typescript
import { useNavigate, useSearch } from '@tanstack/react-router';
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
validateSearch: z.object({
category: z.string().optional(),
sortBy: z.enum(['price', 'name']).default('name'),
page: z.number().default(1),
}),
});
function ProductFilters() {
const navigate = useNavigate({ from: productsRoute.id });
const search = useSearch({ from: productsRoute.id });
const updateCategory = (category: string) => {
navigate({
search: (prev) => ({
...prev,
category, // Type-checked
page: 1, // Reset page
}),
});
};
const updateSort = (sortBy: 'price' | 'name') => {
navigate({
search: (prev) => ({ ...prev, sortBy }),
});
};
return (
<div>
<select value={search.category || ''} onChange={(e) => updateCategory(e.target.value)}>
<option value="">All Categories</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
<select value={search.sortBy} onChange={(e) => updateSort(e.target.value as 'price' | 'name')}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</div>
);
}
```
## Nested Routes
```typescript
const usersRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users',
component: UsersLayout,
});
const usersIndexRoute = createRoute({
getParentRoute: () => usersRoute,
path: '/',
component: UsersList,
});
const userDetailRoute = createRoute({
getParentRoute: () => usersRoute,
path: '$userId',
component: UserDetail,
});
const routeTree = rootRoute.addChildren([
usersRoute.addChildren([
usersIndexRoute,
userDetailRoute,
]),
]);
// Layout component with outlet
function UsersLayout() {
return (
<div>
<h1>Users</h1>
<Outlet /> {/* Renders child route */}
</div>
);
}
```
## Before Load Hook
```typescript
const protectedRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/dashboard',
beforeLoad: async ({ location }) => {
const isAuthenticated = await checkAuth();
if (!isAuthenticated) {
throw redirect({
to: '/login',
search: { redirect: location.href },
});
}
return { user: await getCurrentUser() };
},
component: Dashboard,
});
function Dashboard() {
const { user } = useLoaderData({ from: protectedRoute.id });
// user available from beforeLoad
return <div>Welcome, {user.name}</div>;
}
```
## Error Handling
```typescript
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
errorComponent: UserErrorPage,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
if (!user) {
throw new Error('User not found');
}
return { user };
},
});
function UserErrorPage({ error }: { error: Error }) {
return (
<div>
<h1>Error</h1>
<p>{error.message}</p>
<Link to="/users">Back to users</Link>
</div>
);
}
```
## Pending Component
```typescript
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
component: UserPage,
pendingComponent: UserPageSkeleton,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
function UserPageSkeleton() {
return (
<div>
<div className="skeleton-header" />
<div className="skeleton-body" />
</div>
);
}
```
## Router Context
Share data across all routes.
```typescript
type RouterContext = {
auth: { user: User | null };
queryClient: QueryClient;
};
const rootRoute = createRootRoute<RouterContext>({
component: RootLayout,
});
const router = createRouter({
routeTree,
context: {
auth: { user: null },
queryClient: new QueryClient(),
},
});
// Access in route
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
beforeLoad: ({ context }) => {
// context typed as RouterContext
console.log(context.auth.user);
},
});
```
## Route Masks
Hide actual URL structure.
```typescript
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
});
// Navigate with mask
navigate({
to: '/users/$userId',
params: { userId: '123' },
mask: {
to: '/profile',
},
});
// URL shows /profile but renders /users/123
```
## Search Param Middleware
Transform search params before validation.
```typescript
const productsRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/products',
validateSearch: z.object({
tags: z.array(z.string()).default([]),
}),
loaderDeps: ({ search }) => ({ tags: search.tags }),
loader: async ({ deps }) => {
const products = await fetchProducts({ tags: deps.tags });
return { products };
},
});
// URL: /products?tags=electronics&tags=sale
// search.tags = ['electronics', 'sale']
```
## Type Helpers
```typescript
import type { RouteIds, RouteById } from '@tanstack/react-router';
// Get all route IDs
type AllRouteIds = RouteIds<typeof router>;
// Get specific route type
type UserRoute = RouteById<typeof router, '/users/$userId'>;
// Extract params type
type UserParams = UserRoute['types']['allParams'];
// Extract search type
type UserSearch = UserRoute['types']['fullSearchSchema'];
```
## Preloading Routes
```typescript
import { useRouter } from '@tanstack/react-router';
function ProductCard({ productId }: { productId: string }) {
const router = useRouter();
const preloadProduct = () => {
router.preloadRoute({
to: '/products/$productId',
params: { productId },
});
};
return (
<Link
to="/products/$productId"
params={{ productId }}
onMouseEnter={preloadProduct} // Preload on hover
>
View Product
</Link>
);
}
```
## Route Matching
```typescript
import { useMatches, useMatch } from '@tanstack/react-router';
function Navigation() {
const matches = useMatches();
// Array of all matched routes
const userMatch = useMatch({ from: userRoute.id, shouldThrow: false });
// userMatch is typed, null if not matched
return (
<nav>
{matches.map((match) => (
<span key={match.id}>{match.pathname}</span>
))}
</nav>
);
}
```
## File-Based Routing (Code Generation)
```typescript
// routes/__root.tsx
export const Route = createRootRoute({
component: RootLayout,
});
// routes/index.tsx
export const Route = createFileRoute('/')({
component: HomePage,
});
// routes/users/$userId.tsx
export const Route = createFileRoute('/users/$userId')({
component: UserPage,
loader: async ({ params }) => {
const user = await fetchUser(params.userId);
return { user };
},
});
// Generate route tree with CLI
// npm run generate-routes
// Import generated routes
import { routeTree } from './routeTree.gen';
const router = createRouter({ routeTree });
```
## Integration with React Query
```typescript
import { queryOptions, useQuery } from '@tanstack/react-query';
const userQueryOptions = (userId: string) =>
queryOptions({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const userRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/users/$userId',
loader: ({ context, params }) => {
// Prefetch with React Query
return context.queryClient.ensureQueryData(userQueryOptions(params.userId));
},
component: UserPage,
});
function UserPage() {
const { userId } = useParams({ from: userRoute.id });
// Use same query options in component
const { data: user } = useQuery(userQueryOptions(userId));
return <div>{user?.name}</div>;
}
```