>(
(props, ref) =>
);
// Context without displayName
const ThemeContext = React.createContext<'light' | 'dark'>('light');
```
**Problems:**
- Anonymous components appear as "Anonymous" in React DevTools
- HOCs without displayName hide the wrapped component identity
- Error stack traces show meaningless component names
- React Profiler cannot label components for performance analysis
## Correct
```tsx
// ✅ Good
import React, { forwardRef, memo, createContext } from 'react';
// Named function export - automatically has displayName
export function Button({ children }: { children: React.ReactNode }): React.ReactElement {
return {children} ;
}
// Arrow function with explicit displayName
export const IconButton = ({ icon, label }: { icon: React.ReactNode; label: string }): React.ReactElement => {
return (
{icon}
);
};
IconButton.displayName = 'IconButton';
// HOC with proper displayName
function withLogger(
WrappedComponent: React.ComponentType
) {
const displayName = WrappedComponent.displayName ?? WrappedComponent.name ?? 'Component';
function WithLogger(props: P) {
console.log(`Rendering ${displayName}:`, props);
return ;
}
WithLogger.displayName = `withLogger(${displayName})`;
return WithLogger;
}
const LoggedButton = withLogger(Button);
// Memo with displayName
interface ListProps {
items: string[];
onItemClick?: (item: string) => void;
}
const ExpensiveList = memo(({ items, onItemClick }) => {
return (
{items.map((item) => (
onItemClick?.(item)}>
{item}
))}
);
});
ExpensiveList.displayName = 'ExpensiveList';
// ForwardRef with displayName
interface InputProps extends Omit, 'size'> {
size?: 'sm' | 'md' | 'lg';
error?: string;
}
const Input = forwardRef(
({ size = 'md', error, className, ...rest }, ref) => {
return (
{error && {error} }
);
}
);
Input.displayName = 'Input';
// Context with displayName
interface ThemeContextValue {
theme: 'light' | 'dark';
toggleTheme: () => void;
}
const ThemeContext = createContext(null);
ThemeContext.displayName = 'ThemeContext';
// Helper function for creating displayName
function getDisplayName(Component: React.ComponentType
): string {
return Component.displayName ?? Component.name ?? 'Component';
}
```
**Benefits:**
- Components appear with readable names in React DevTools
- Stack traces show meaningful component names
- Wrapped components show their hierarchy (e.g., "withLogger(Button)")
- React Profiler shows component names for performance analysis
- Component names appear in test output and snapshots
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## Rest Props Typing
**Impact: CRITICAL (ensures type-safe prop spreading to HTML elements)**
Typing rest/spread props for components that pass through HTML attributes. Proper Omit patterns prevent prop conflicts and maintain full type safety.
## Incorrect
```tsx
// ❌ Bad
// Using 'any' for rest props
interface ButtonProps {
variant: 'primary' | 'secondary';
[key: string]: any; // Loses all type safety
}
const Button = ({ variant, ...rest }: ButtonProps) => {
return ;
};
// Not properly omitting custom props from HTML props
interface InputProps extends React.InputHTMLAttributes {
label: string;
error: string;
}
const Input = ({ label, error, ...rest }: InputProps) => {
// 'label' and 'error' are valid HTML attributes, causing conflicts
return (
{label}
{error}
);
};
```
**Problems:**
- Index signatures like `[key: string]: any` remove all type checking on spread props
- Not omitting custom prop names that collide with HTML attributes causes conflicts
- Missing `ComponentPropsWithoutRef` can lead to ref handling issues
## Correct
```tsx
// ✅ Good
// Properly extending and omitting HTML attributes
interface ButtonProps extends Omit, 'className'> {
variant: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
}
function Button({
variant,
size = 'md',
children,
...rest
}: ButtonProps): React.ReactElement {
return (
{children}
);
}
// Using ComponentPropsWithoutRef for better ref handling
import { ComponentPropsWithoutRef } from 'react';
interface CustomInputProps {
label: string;
errorMessage?: string;
helperText?: string;
}
type InputProps = CustomInputProps & Omit, keyof CustomInputProps>;
function CustomInput({
label,
errorMessage,
helperText,
id,
...rest
}: InputProps): React.ReactElement {
const inputId = id ?? `input-${label.toLowerCase().replace(/\s/g, '-')}`;
return (
{label}
{errorMessage && {errorMessage} }
{helperText && !errorMessage && {helperText} }
);
}
// Creating reusable base prop types
type BaseButtonProps = ComponentPropsWithoutRef<'button'>;
interface IconButtonProps extends Omit {
icon: React.ReactNode;
'aria-label': string; // Required for accessibility
}
function IconButton({ icon, ...rest }: IconButtonProps): React.ReactElement {
return {icon} ;
}
// Generic rest props for polymorphic components
type AsProp = {
as?: C;
};
type PropsToOmit = keyof (AsProp & P);
type PolymorphicComponentProps<
C extends React.ElementType,
Props = object
> = Props &
AsProp &
Omit, PropsToOmit>;
interface BoxOwnProps {
padding?: 'sm' | 'md' | 'lg';
margin?: 'sm' | 'md' | 'lg';
}
type BoxProps = PolymorphicComponentProps;
function Box({
as,
padding,
margin,
className,
...rest
}: BoxProps): React.ReactElement {
const Component = as ?? 'div';
const classes = [
className,
padding && `p-${padding}`,
margin && `m-${margin}`,
].filter(Boolean).join(' ');
return ;
}
// Usage with full type safety
Content in div
Content in section
Link with padding
// Spreading with explicit rest type annotation
interface CardProps extends Omit, 'title'> {
title: React.ReactNode; // Override HTML title attribute
subtitle?: string;
}
function Card({
title,
subtitle,
children,
...divProps
}: CardProps): React.ReactElement {
return (
{title}
{subtitle && {subtitle}
}
{children}
);
}
```
**Benefits:**
- Proper typing catches invalid props at compile time
- IDE shows valid HTML attributes when using the component
- Omit removes props that conflict with custom props
- `ComponentPropsWithoutRef` properly excludes ref from spread
- Types clearly show which props are custom vs passed through
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## useState Hook Typing
**Impact: CRITICAL (prevents type errors when initial values do not represent all possible states)**
useState infers types from initial values. When the initial value does not represent all possible states (like `null` for async data), explicit typing prevents runtime errors.
## Incorrect
```tsx
// ❌ Bad
// Inferred as null, can't set to User
const [user, setUser] = useState(null)
setUser({ id: 1, name: 'John' }) // Error!
// Inferred as never[] - can't add typed items
const [items, setItems] = useState([])
setItems([{ id: 1 }]) // Error!
// Inferred as string, can't set undefined
const [search, setSearch] = useState('')
setSearch(undefined) // Error if you need undefined state
```
**Problems:**
- `useState(null)` infers type as `null` only, rejecting object values
- `useState([])` infers type as `never[]`, rejecting any array items
- Simple type inference cannot represent union states like `string | undefined`
## Correct
```tsx
// ✅ Good
// Nullable State
interface User {
id: number
name: string
email: string
}
// Explicit union type for nullable state
const [user, setUser] = useState(null)
// Now both work:
setUser({ id: 1, name: 'John', email: 'john@example.com' })
setUser(null)
// Access with null check
if (user) {
console.log(user.name) // TypeScript knows user is User here
}
// Array State
interface Todo {
id: number
text: string
done: boolean
}
const [todos, setTodos] = useState([])
// Add item
setTodos(prev => [...prev, { id: Date.now(), text: 'New', done: false }])
// Update item
setTodos(prev =>
prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
)
)
// Remove item
setTodos(prev => prev.filter(todo => todo.id !== id))
// Object State
interface FormData {
name: string
email: string
age: number
}
const [form, setForm] = useState({
name: '',
email: '',
age: 0,
})
// Update single field
setForm(prev => ({ ...prev, name: 'John' }))
// Partial updates helper
const updateForm = (
field: K,
value: FormData[K]
) => {
setForm(prev => ({ ...prev, [field]: value }))
}
updateForm('name', 'John')
updateForm('age', 25)
// Union State (Discriminated union for state machines)
type RequestState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
const [state, setState] = useState>({ status: 'idle' })
// Usage
switch (state.status) {
case 'idle':
return Load
case 'loading':
return
case 'success':
return // data is typed!
case 'error':
return
}
// Lazy Initialization
const [lazyState, setLazyState] = useState(() => {
return computeInitialState()
})
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
const saved = localStorage.getItem('theme')
return (saved as 'light' | 'dark') || 'light'
})
// Undefined vs Null
// Use undefined for "not yet set"
const [selectedId, setSelectedId] = useState(undefined)
// Use null for "explicitly empty"
// (same pattern as the nullable state example above)
const [userData, setUserData] = useState(null)
```
**Benefits:**
- Explicit type annotations allow state to hold values beyond the initial type
- Discriminated unions enable exhaustive state machine patterns
- Lazy initialization runs expensive computations only once
- Clear conventions for `null` vs `undefined` improve code readability
- Generic type parameters work seamlessly with complex state shapes
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## useRef Hook Typing
**Impact: CRITICAL (prevents null reference errors and unnecessary null checks)**
useRef has two distinct use cases with different typing: DOM element refs (nullable) and mutable value storage (non-nullable). Using the wrong pattern causes type errors or requires unnecessary null checks.
## Incorrect
```tsx
// ❌ Bad
// Missing element type
const inputRef = useRef(null)
inputRef.current.focus() // Error: possibly null
// Wrong initial value for DOM ref
const inputRef = useRef() // undefined, not null
// Type error
// Treating mutable ref as nullable
const countRef = useRef(0)
if (countRef.current !== null) { // Unnecessary check
countRef.current++
}
```
**Problems:**
- Missing element type generic means `current` has no useful properties
- Using `undefined` instead of `null` for DOM refs causes type incompatibility
- Unnecessary null checks on mutable refs add noise and reduce readability
## Correct
```tsx
// ✅ Good
// DOM Element Refs - pass null, type the element
// const inputRef = useRef(null)
// const buttonRef = useRef(null)
// const divRef = useRef(null)
function Form() {
const inputRef = useRef(null)
const focusInput = () => {
// Optional chaining because ref might not be attached yet
inputRef.current?.focus()
}
useEffect(() => {
inputRef.current?.focus()
}, [])
return
}
// Common DOM Element Types (standalone declarations for reference)
// const inputRef = useRef(null)
// const textareaRef = useRef(null)
// const selectRef = useRef(null)
// const formRef = useRef(null)
// const divRef = useRef(null)
// const videoRef = useRef(null)
// const canvasRef = useRef(null)
// const svgRef = useRef(null)
// Mutable Value Refs - pass actual initial value
function Timer() {
const intervalRef = useRef(undefined)
const countRef = useRef(0) // Inferred as MutableRefObject
useEffect(() => {
intervalRef.current = window.setInterval(() => {
countRef.current++ // No null check needed
}, 1000)
return () => {
clearInterval(intervalRef.current)
}
}, [])
}
// Storing Previous Value
function usePrevious(value: T): T | undefined {
const ref = useRef(undefined)
useEffect(() => {
ref.current = value
}, [value])
return ref.current
}
function Counter({ count }: { count: number }) {
const prevCount = usePrevious(count)
return (
Current: {count}, Previous: {prevCount ?? 'none'}
)
}
// Storing Callbacks
function useEventCallback unknown>(fn: T): T {
const ref = useRef(fn)
useEffect(() => {
ref.current = fn
}, [fn])
return useCallback(
((...args) => ref.current(...args)) as T,
[]
)
}
// Multiple Refs (Callback Refs)
interface Item { id: string; name: string }
function List({ items }: { items: Item[] }) {
const itemRefs = useRef>(new Map())
const setRef = (id: string) => (el: HTMLLIElement | null) => {
if (el) {
itemRefs.current.set(id, el)
} else {
itemRefs.current.delete(id)
}
}
const scrollToItem = (id: string) => {
itemRefs.current.get(id)?.scrollIntoView()
}
return (
{items.map(item => (
{item.name}
))}
)
}
```
**Benefits:**
- DOM refs with `null` initial value create `RefObject` with nullable current
- Mutable refs with actual initial value create `MutableRefObject` with non-nullable current
- Optional chaining on DOM refs handles the "not yet attached" state cleanly
- Callback refs enable dynamic ref management for lists of elements
- Ref-stored callbacks avoid stale closure issues without triggering re-renders
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## useReducer Typing
**Impact: CRITICAL (enables exhaustive action handling and type-safe state transitions)**
Properly typing the useReducer hook with actions, state, and discriminated unions. Discriminated union action types enable exhaustive switch statements and catch missing cases at compile time.
## Incorrect
```tsx
// ❌ Bad
// Untyped reducer - no type safety
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
default:
return state;
}
};
// Action type as string - no autocomplete or validation
interface Action {
type: string;
payload?: any;
}
// Missing exhaustiveness checking
function badReducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return { ...state, items: [...state.items, action.payload] };
// Forgot to handle 'remove' action - no error!
}
return state;
}
```
**Problems:**
- Untyped reducers have implicit `any` for state and action
- String-typed action types provide no autocomplete or validation
- Without exhaustiveness checking, missing action cases are silently ignored
- `any` payload types allow invalid data to pass through
## Correct
```tsx
// ✅ Good
import { useReducer, Reducer } from 'react';
// Define state interface
interface CounterState {
count: number;
step: number;
}
// Define action types using discriminated union
type CounterAction =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset' }
| { type: 'setStep'; payload: number }
| { type: 'incrementBy'; payload: number };
const initialState: CounterState = {
count: 0,
step: 1,
};
// Typed reducer with exhaustiveness checking
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'reset':
return initialState;
case 'setStep':
return { ...state, step: action.payload };
case 'incrementBy':
return { ...state, count: state.count + action.payload };
default:
// Exhaustiveness check - will error if a case is missing
const _exhaustive: never = action;
return state;
}
}
// Usage in component
function Counter() {
const [state, dispatch] = useReducer(counterReducer, initialState);
return (
Count: {state.count}
dispatch({ type: 'increment' })}>+
dispatch({ type: 'decrement' })}>-
dispatch({ type: 'reset' })}>Reset
dispatch({ type: 'incrementBy', payload: 10 })}>+10
);
}
// Complex state with nested objects
interface TodoState {
todos: Todo[];
filter: 'all' | 'active' | 'completed';
isLoading: boolean;
error: string | null;
}
interface Todo {
id: string;
text: string;
completed: boolean;
}
type TodoAction =
| { type: 'ADD_TODO'; payload: { text: string } }
| { type: 'TOGGLE_TODO'; payload: { id: string } }
| { type: 'DELETE_TODO'; payload: { id: string } }
| { type: 'SET_FILTER'; payload: TodoState['filter'] }
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: Todo[] }
| { type: 'FETCH_ERROR'; payload: string };
function todoReducer(state: TodoState, action: TodoAction): TodoState {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: crypto.randomUUID(),
text: action.payload.text,
completed: false,
},
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.id
? { ...todo, completed: !todo.completed }
: todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
case 'SET_FILTER':
return { ...state, filter: action.payload };
case 'FETCH_START':
return { ...state, isLoading: true, error: null };
case 'FETCH_SUCCESS':
return { ...state, isLoading: false, todos: action.payload };
case 'FETCH_ERROR':
return { ...state, isLoading: false, error: action.payload };
default:
const _exhaustive: never = action;
return state;
}
}
// Lazy initialization with typed init function
interface FormState {
values: Record;
touched: Record;
errors: Record;
}
type FormAction =
| { type: 'SET_VALUE'; field: string; value: string }
| { type: 'SET_TOUCHED'; field: string }
| { type: 'SET_ERROR'; field: string; error: string }
| { type: 'RESET' };
function createInitialState(fields: string[]): FormState {
return {
values: Object.fromEntries(fields.map((f) => [f, ''])),
touched: Object.fromEntries(fields.map((f) => [f, false])),
errors: {},
};
}
function formReducer(state: FormState, action: FormAction): FormState {
switch (action.type) {
case 'SET_VALUE':
return {
...state,
values: { ...state.values, [action.field]: action.value },
};
case 'SET_TOUCHED':
return {
...state,
touched: { ...state.touched, [action.field]: true },
};
case 'SET_ERROR':
return {
...state,
errors: { ...state.errors, [action.field]: action.error },
};
case 'RESET':
return createInitialState(Object.keys(state.values));
default:
const _exhaustive: never = action;
return state;
}
}
// Usage with lazy init
function Form({ fields }: { fields: string[] }) {
const [state, dispatch] = useReducer(formReducer, fields, createInitialState);
// dispatch is fully typed
dispatch({ type: 'SET_VALUE', field: 'email', value: 'test@example.com' });
return ;
}
// Action creators for better DX
const todoActions = {
addTodo: (text: string): TodoAction => ({ type: 'ADD_TODO', payload: { text } }),
toggleTodo: (id: string): TodoAction => ({ type: 'TOGGLE_TODO', payload: { id } }),
deleteTodo: (id: string): TodoAction => ({ type: 'DELETE_TODO', payload: { id } }),
setFilter: (filter: TodoState['filter']): TodoAction => ({ type: 'SET_FILTER', payload: filter }),
};
// Usage with action creators
dispatch(todoActions.addTodo('Buy groceries'));
dispatch(todoActions.toggleTodo('123'));
```
**Benefits:**
- Discriminated unions enable exhaustive switch statements
- Compile-time errors when action cases are missing
- Each action has its own strongly-typed payload
- Type system helps ensure proper immutable state updates
- Lazy initialization third argument properly types the init function
- Action creator factory functions provide better developer experience
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## useCallback Typing
**Impact: CRITICAL (ensures memoized callbacks have correct parameter and return types)**
Properly typing useCallback for memoized function references. Explicit parameter types enable autocomplete and catch errors, while proper dependencies prevent stale closures.
## Incorrect
```tsx
// ❌ Bad
// Missing dependency array type inference
const handleClick = useCallback((id) => {
console.log(id); // id is implicitly 'any'
}, []);
// Incorrect return type inference
const fetchData = useCallback(async () => {
const data = await api.getData();
return data; // Return type not enforced
});
// Dependencies not aligned with closure usage
const [count, setCount] = useState(0);
const increment = useCallback(() => {
setCount(count + 1); // Stale closure - count not in deps
}, []); // Missing count dependency
// Using 'any' for event parameter
const handleChange = useCallback((e: any) => {
setValue(e.target.value);
}, []);
```
**Problems:**
- Missing parameter types result in implicit `any` with no autocomplete
- Missing or incorrect dependencies cause stale closure bugs
- Using `any` for event parameters removes all type checking
- Return types are not enforced without explicit annotation
## Correct
```tsx
// ✅ Good
import { useCallback, useState } from 'react';
// Basic callback with explicit parameter types
const handleClick = useCallback((id: string) => {
console.log('Clicked:', id);
}, []);
// Callback with event typing
const handleInputChange = useCallback(
(event: React.ChangeEvent) => {
const value = event.target.value;
console.log('Input changed:', value);
},
[]
);
// Callback with return type
interface CartItem { id: string; name: string; price: number; quantity: number }
const calculateTotal = useCallback(
(items: CartItem[]): number => {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
[]
);
// Async callback with proper typing
interface User {
id: string;
name: string;
email: string;
}
const fetchUser = useCallback(
async (userId: string): Promise => {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
},
[]
);
// Callback using state with proper dependencies
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
// Use functional update to avoid stale closure
const increment = useCallback(() => {
setCount((prevCount) => prevCount + step);
}, [step]); // Only depends on step, count uses functional update
// When you need the current value in the callback
const logAndIncrement = useCallback(() => {
console.log('Current count:', count);
setCount((prevCount) => prevCount + 1);
}, [count]); // count needed for console.log
return (
);
}
// Callback passed to child with proper typing
interface Item { id: string; name: string }
interface ItemListProps {
items: Item[];
onItemSelect: (item: Item) => void;
onItemDelete: (id: string) => Promise;
}
function ItemContainer() {
const [items, setItems] = useState- ([]);
const handleSelect = useCallback((item: Item) => {
console.log('Selected:', item.name);
}, []);
const handleDelete = useCallback(async (id: string): Promise
=> {
// assume api is injected or imported
await api.deleteItem(id);
setItems((prev) => prev.filter((item) => item.id !== id));
}, []);
return (
);
}
// Callback with multiple parameters
interface FormData {
name: string;
email: string;
message: string;
}
type FormField = keyof FormData;
function FormComponent() {
const [formData, setFormData] = useState>({});
const handleFieldChange = useCallback(
(field: FormField, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
},
[]
);
}
// Memoized callback for optimized child renders
const MemoizedChild = React.memo<{ onClick: () => void }>(({ onClick }) => {
console.log('Child rendered');
return Click me ;
});
function Parent() {
const [count, setCount] = useState(0);
// Without useCallback, MemoizedChild would re-render on every Parent render
const handleClick = useCallback(() => {
setCount((c) => c + 1);
}, []);
return (
);
}
```
**Benefits:**
- Explicit parameter types enable autocomplete and catch errors
- Declared return types ensure callbacks return expected values
- Functional updates avoid stale closures without adding state to deps
- Proper React event types for form and DOM events
- Stable references prevent unnecessary child re-renders with React.memo
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## useMemo Typing
**Impact: CRITICAL (ensures memoized values have correct types and proper dependencies)**
Properly typing useMemo for memoized computed values. Explicit type annotations make code more readable, catch errors, and ensure correct dependency arrays.
## Incorrect
```tsx
// ❌ Bad
// Missing type annotation - relies entirely on inference
const expensiveResult = useMemo(() => {
return someExpensiveCalculation(data);
}, [data]);
// Using 'any' loses type safety
const config = useMemo(() => ({
theme: 'dark',
features: ['a', 'b'],
}), []);
// Unnecessary useMemo for simple values
const double = useMemo(() => count * 2, [count]); // Simple math doesn't need memoization
// Missing dependencies causes stale values
const filteredItems = useMemo(() => {
return items.filter(item => item.category === selectedCategory);
}, []); // Missing items and selectedCategory
```
**Problems:**
- Using `any` removes type safety on the memoized value
- Missing dependencies cause stale cached values
- Simple calculations do not benefit from memoization overhead
- Unclear return types when conditionally returning different shapes
## Correct
```tsx
// ✅ Good
import { useMemo, useState, createContext } from 'react';
// Basic useMemo with explicit type
interface ProcessedData {
total: number;
average: number;
max: number;
min: number;
}
const statistics = useMemo(() => {
const total = numbers.reduce((sum, n) => sum + n, 0);
return {
total,
average: total / numbers.length,
max: Math.max(...numbers),
min: Math.min(...numbers),
};
}, [numbers]);
// Filtering and sorting with proper typing
interface Product {
id: string;
name: string;
price: number;
category: string;
inStock: boolean;
}
type SortField = 'name' | 'price';
type SortDirection = 'asc' | 'desc';
function ProductList({ products }: { products: Product[] }) {
const [searchTerm, setSearchTerm] = useState('');
const [category, setCategory] = useState(null);
const [sortField, setSortField] = useState('name');
const [sortDirection, setSortDirection] = useState('asc');
const filteredAndSortedProducts = useMemo(() => {
let result = products;
if (searchTerm) {
const term = searchTerm.toLowerCase();
result = result.filter((p) =>
p.name.toLowerCase().includes(term)
);
}
if (category) {
result = result.filter((p) => p.category === category);
}
result = [...result].sort((a, b) => {
const aVal = a[sortField];
const bVal = b[sortField];
const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
return sortDirection === 'asc' ? comparison : -comparison;
});
return result;
}, [products, searchTerm, category, sortField, sortDirection]);
return (
{filteredAndSortedProducts.map((product) => (
{product.name} - ${product.price}
))}
);
}
// Memoizing derived state for context
interface ThemeColors {
primary: string;
secondary: string;
background: string;
text: string;
border: string;
}
interface ThemeContextValue {
theme: 'light' | 'dark';
colors: ThemeColors;
}
const ThemeContext = createContext(null!);
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const colors = useMemo(() => {
if (theme === 'light') {
return {
primary: '#007bff',
secondary: '#6c757d',
background: '#ffffff',
text: '#212529',
border: '#dee2e6',
};
}
return {
primary: '#6ea8fe',
secondary: '#adb5bd',
background: '#212529',
text: '#ffffff',
border: '#495057',
};
}, [theme]);
const contextValue = useMemo(
() => ({ theme, colors }),
[theme, colors]
);
return (
{children}
);
}
// Memoizing expensive transformations
interface RawDataPoint {
timestamp: string;
value: number;
metadata: Record;
}
interface ChartDataPoint {
x: Date;
y: number;
label: string;
}
function DataVisualization({ rawData }: { rawData: RawDataPoint[] }) {
const chartData = useMemo(() => {
return rawData.map((point) => ({
x: new Date(point.timestamp),
y: point.value,
label: `Value: ${point.value}`,
}));
}, [rawData]);
const aggregatedData = useMemo(() => {
const byMonth = new Map();
chartData.forEach((point) => {
const monthKey = `${point.x.getFullYear()}-${point.x.getMonth()}`;
const existing = byMonth.get(monthKey) ?? [];
byMonth.set(monthKey, [...existing, point.y]);
});
return Array.from(byMonth.entries()).map(([month, values]) => ({
month,
average: values.reduce((a, b) => a + b, 0) / values.length,
count: values.length,
}));
}, [chartData]);
return ;
}
// Generic memoized selector pattern
interface Order { id: string; total: number }
function useMemoizedSelector(
state: TState,
selector: (state: TState) => TSelected
): TSelected {
return useMemo(() => selector(state), [state, selector]);
}
// Usage
interface User {
id: string;
name: string;
email: string;
isActive: boolean;
}
interface AppState {
users: User[];
products: Product[];
orders: Order[];
}
function UserList({ state }: { state: AppState }) {
const activeUsers = useMemoizedSelector(
state,
(s) => s.users.filter((u) => u.isActive)
);
return {activeUsers.map((u) => {u.name} )} ;
}
```
**Benefits:**
- Explicit types make code more readable and catch type mismatches
- Correct dependencies ensure memoized values stay up to date
- Only expensive calculations benefit from memoization
- Reference stability prevents unnecessary child re-renders
- Derived state avoids storing redundant data
- Memoized context values prevent provider-triggered re-renders
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## useContext Typing
**Impact: CRITICAL (prevents silent failures from undefined context access)**
Properly typing Context and useContext for type-safe global state. Using null defaults with custom guard hooks prevents silent failures when context is used outside its provider.
## Incorrect
```tsx
// ❌ Bad
// Untyped context - no type safety
const AppContext = React.createContext(undefined);
// Using 'any' for context value
const ThemeContext = React.createContext({ theme: 'light' });
// Default value that doesn't match actual usage
interface User {
id: string;
name: string;
}
const UserContext = React.createContext({
id: '',
name: '',
}); // Fake default encourages using context outside provider
// Not handling missing provider
const AuthContext = React.createContext(undefined);
function useAuth() {
const context = React.useContext(AuthContext);
return context; // Could be undefined, but callers don't know
}
```
**Problems:**
- Untyped context defaults to `any` with no type checking
- Using `any` removes autocomplete and error detection
- Fake default values mask missing providers and create misleading behavior
- Returning potentially undefined context without a guard leaves callers unprotected
## Correct
```tsx
// ✅ Good
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
// Pattern 1: Context with null default and custom hook
interface User {
id: string;
name: string;
email: string;
}
interface AuthContextValue {
user: User | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise;
logout: () => void;
}
const AuthContext = createContext(null);
AuthContext.displayName = 'AuthContext';
function useAuth(): AuthContextValue {
const context = useContext(AuthContext);
if (context === null) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
// assume: const api = createApiClient()
function AuthProvider({ children }: { children: React.ReactNode }): React.ReactElement {
const [user, setUser] = useState(null);
const login = useCallback(async (email: string, password: string) => {
const response = await api.login(email, password);
setUser(response.user);
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const value = useMemo(
() => ({
user,
isAuthenticated: user !== null,
login,
logout,
}),
[user, login, logout]
);
return {children} ;
}
// Usage in component
function UserProfile(): React.ReactElement {
const { user, logout } = useAuth(); // Guaranteed to be non-null
if (!user) {
return Please log in
;
}
return (
{user.name}
{user.email}
Logout
);
}
// Pattern 2: Multiple contexts for separation of concerns
interface ThemeContextValue {
theme: 'light' | 'dark';
colors: {
primary: string;
background: string;
text: string;
};
}
interface ThemeActionsContextValue {
setTheme: (theme: 'light' | 'dark') => void;
toggleTheme: () => void;
}
const ThemeContext = createContext(null);
const ThemeActionsContext = createContext(null);
function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within ThemeProvider');
}
return context;
}
function useThemeActions(): ThemeActionsContextValue {
const context = useContext(ThemeActionsContext);
if (!context) {
throw new Error('useThemeActions must be used within ThemeProvider');
}
return context;
}
// Pattern 3: Generic context factory
function createSafeContext(displayName: string) {
const Context = createContext(null);
Context.displayName = displayName;
function useContextSafe(): T {
const context = useContext(Context);
if (context === null) {
throw new Error(`use${displayName} must be used within ${displayName}Provider`);
}
return context;
}
return [Context.Provider, useContextSafe] as const;
}
// Usage of factory
interface AppNotification { id: string; message: string; type: 'info' | 'error' | 'success' }
interface NotificationContextValue {
notifications: AppNotification[];
addNotification: (notification: Omit) => void;
removeNotification: (id: string) => void;
}
const [NotificationProvider, useNotifications] = createSafeContext('Notification');
// Pattern 4: Context with reducer
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
type CartAction =
| { type: 'ADD_ITEM'; payload: Omit }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'UPDATE_QUANTITY'; payload: { id: string; quantity: number } }
| { type: 'CLEAR' };
interface CartState {
items: CartItem[];
total: number;
}
interface CartContextValue {
state: CartState;
dispatch: React.Dispatch;
itemCount: number;
addItem: (item: Omit) => void;
removeItem: (id: string) => void;
}
const CartContext = createContext(null);
function useCart(): CartContextValue {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within CartProvider');
}
return context;
}
```
**Benefits:**
- Null default with error boundary prevents silent failures
- Custom hooks encapsulate context access and error handling
- Separated contexts split state and actions to prevent unnecessary re-renders
- Factory pattern reduces boilerplate for creating typed contexts
- Memoized provider values prevent unnecessary re-renders
- DisplayName improves debugging in React DevTools
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## Custom Hooks Typing
**Impact: CRITICAL (ensures clear hook contracts with proper return types and parameters)**
Creating properly typed custom hooks with clear return types and parameters. Explicit typing makes hook contracts clear to consumers and catches errors early.
## Incorrect
```tsx
// ❌ Bad
// No return type - callers don't know what to expect
function useUser(id) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
fetchUser(id).then(setUser).finally(() => setLoading(false));
}, [id]);
return { user, loading };
}
// Using 'any' for state
function useLocalStorage(key: string, initialValue: any) {
const [value, setValue] = useState(initialValue);
return [value, setValue];
}
// Inconsistent return type (object vs tuple)
function useToggle(initial: boolean) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(v => !v);
return { value, toggle, setValue };
}
// Missing cleanup and error handling
function useEventListener(event: string, handler: any) {
useEffect(() => {
window.addEventListener(event, handler);
}, [event, handler]);
}
```
**Problems:**
- Missing parameter types result in implicit `any`
- No return type annotation makes the hook contract unclear
- Using `any` for state removes all type safety
- Missing effect cleanup causes memory leaks
- Missing error handling in async hooks leads to uncaught errors
## Correct
```tsx
// ✅ Good
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
// Hook with clear parameter and return types
interface User {
id: string;
name: string;
email: string;
}
interface UseUserResult {
user: User | null;
loading: boolean;
error: Error | null;
refetch: () => Promise;
}
function useUser(userId: string | null): UseUserResult {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUserData = useCallback(async () => {
if (!userId) {
setUser(null);
return;
}
setLoading(true);
setError(null);
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data: User = await response.json();
setUser(data);
} catch (err) {
setError(err instanceof Error ? err : new Error('Unknown error'));
setUser(null);
} finally {
setLoading(false);
}
}, [userId]);
useEffect(() => {
fetchUserData();
}, [fetchUserData]);
return { user, loading, error, refetch: fetchUserData };
}
// Generic hook with type parameter
function useLocalStorage(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback(
(value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const valueToStore = value instanceof Function ? value(prev) : value;
localStorage.setItem(key, JSON.stringify(valueToStore));
return valueToStore;
});
},
[key]
);
const removeValue = useCallback(() => {
localStorage.removeItem(key);
setStoredValue(initialValue);
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}
// Usage with explicit type
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
// Tuple return type for toggle hook
function useToggle(
initialValue: boolean = false
): [boolean, () => void, (value: boolean) => void] {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue((v) => !v);
}, []);
const setValueDirectly = useCallback((newValue: boolean) => {
setValue(newValue);
}, []);
return [value, toggle, setValueDirectly];
}
// Event listener hook with proper typing
function useEventListener(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element: Window | null = window,
options?: AddEventListenerOptions
): void {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
if (!element) return;
const eventListener = (event: WindowEventMap[K]) => {
savedHandler.current(event);
};
element.addEventListener(eventName, eventListener, options);
return () => {
element.removeEventListener(eventName, eventListener, options);
};
}, [eventName, element, options]);
}
// Async hook with proper state machine
type AsyncState =
| { status: 'idle'; data: null; error: null }
| { status: 'loading'; data: null; error: null }
| { status: 'success'; data: T; error: null }
| { status: 'error'; data: null; error: Error };
interface UseAsyncResult {
state: AsyncState;
execute: () => Promise;
reset: () => void;
}
function useAsync(
asyncFunction: () => Promise,
immediate: boolean = true
): UseAsyncResult {
const [state, setState] = useState>({
status: 'idle',
data: null,
error: null,
});
const execute = useCallback(async (): Promise => {
setState({ status: 'loading', data: null, error: null });
try {
const data = await asyncFunction();
setState({ status: 'success', data, error: null });
return data;
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error');
setState({ status: 'error', data: null, error: err });
return null;
}
}, [asyncFunction]);
const reset = useCallback(() => {
setState({ status: 'idle', data: null, error: null });
}, []);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { state, execute, reset };
}
// Hook returning readonly tuple (const assertion)
function useCounter(initialValue: number = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => setCount((c) => c + 1), []);
const decrement = useCallback(() => setCount((c) => c - 1), []);
const reset = useCallback(() => setCount(initialValue), [initialValue]);
return [count, { increment, decrement, reset }] as const;
}
// Usage - tuple is readonly [number, { increment, decrement, reset }]
const [count, { increment, decrement, reset }] = useCounter(0);
```
**Benefits:**
- Explicit return types make hook contracts clear to consumers
- Generic parameters enable type-safe reuse across different data types
- Tuple returns for simple hooks, objects for complex ones
- Proper cleanup in effects prevents memory leaks
- Error states in async hooks prevent uncaught runtime errors
- Ref patterns store values that should not trigger re-renders
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## Generic Hooks Typing
**Impact: CRITICAL (enables flexible, reusable hooks without losing type safety)**
Creating flexible, reusable hooks with TypeScript generics. Well-designed generics preserve type information throughout the hook without requiring explicit type arguments.
## Incorrect
```tsx
// ❌ Bad
// Using 'any' instead of generics
function useFetch(url: string): { data: any; loading: boolean } {
const [data, setData] = useState(null);
// ...
return { data, loading };
}
// Not constraining generic types when needed
function useList(initial: T[]) {
const [items, setItems] = useState(initial);
const add = (item: T) => setItems([...items, item]);
const remove = (item: T) => setItems(items.filter(i => i === item)); // Needs ID
return { items, add, remove };
}
// Overly complex generics that are hard to understand
function useStore<
T extends Record,
K extends keyof T,
V extends T[K],
A extends { type: string; payload: V }
>(initial: T): [T, (action: A) => void] {
// Too many type parameters
}
```
**Problems:**
- Using `any` removes all type information from hook return values
- Unconstrained generics allow operations that require specific shapes (like `id`)
- Too many type parameters make hooks difficult to use and understand
## Correct
```tsx
// ✅ Good
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
// Basic generic hook for data fetching
interface FetchState {
data: T | null;
loading: boolean;
error: Error | null;
}
interface UseFetchOptions {
initialData?: T;
onSuccess?: (data: T) => void;
onError?: (error: Error) => void;
transform?: (raw: unknown) => T;
}
function useFetch(
url: string | null,
options: UseFetchOptions = {}
): FetchState & { refetch: () => Promise } {
const { initialData = null, onSuccess, onError, transform } = options;
const [state, setState] = useState>({
data: initialData,
loading: !!url,
error: null,
});
const fetchData = useCallback(async () => {
if (!url) {
setState({ data: initialData, loading: false, error: null });
return;
}
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const raw = await response.json();
const data = transform ? transform(raw) : (raw as T);
setState({ data, loading: false, error: null });
onSuccess?.(data);
} catch (err) {
const error = err instanceof Error ? err : new Error('Fetch failed');
setState({ data: null, loading: false, error });
onError?.(error);
}
}, [url, initialData, transform, onSuccess, onError]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { ...state, refetch: fetchData };
}
// Usage with type inference
interface User {
id: string;
name: string;
email: string;
}
const { data: user, loading, error } = useFetch('/api/user/1');
// user is User | null
// Generic list management hook with proper constraints
interface Identifiable {
id: string | number;
}
interface UseListActions {
add: (item: T) => void;
remove: (id: T extends Identifiable ? T['id'] : number) => void;
update: (id: T extends Identifiable ? T['id'] : number, updates: Partial) => void;
clear: () => void;
set: (items: T[]) => void;
}
function useList(
initialItems: T[] = []
): [T[], UseListActions] {
const [items, setItems] = useState(initialItems);
const actions: UseListActions = useMemo(
() => ({
add: (item: T) => {
setItems((prev) => [...prev, item]);
},
remove: (id) => {
setItems((prev) => prev.filter((item) => item.id !== id));
},
update: (id, updates) => {
setItems((prev) =>
prev.map((item) => (item.id === id ? { ...item, ...updates } : item))
);
},
clear: () => {
setItems([]);
},
set: (newItems) => {
setItems(newItems);
},
}),
[]
);
return [items, actions];
}
// Usage
interface Todo {
id: string;
text: string;
completed: boolean;
}
const [todos, { add, remove, update }] = useList([]);
add({ id: '1', text: 'Learn TypeScript', completed: false });
update('1', { completed: true });
remove('1');
// Generic form hook
type ValidationRule = (value: T) => string | null;
interface UseFormOptions> {
initialValues: T;
validationRules?: Partial<{ [K in keyof T]: ValidationRule }>;
onSubmit: (values: T) => void | Promise;
}
interface UseFormResult> {
values: T;
errors: Partial>;
touched: Partial>;
isSubmitting: boolean;
isValid: boolean;
handleChange: (field: K, value: T[K]) => void;
handleBlur: (field: K) => void;
handleSubmit: (e?: React.FormEvent) => Promise;
reset: () => void;
setFieldValue: (field: K, value: T[K]) => void;
setFieldError: (field: K, error: string) => void;
}
function useForm>({
initialValues,
validationRules = {},
onSubmit,
}: UseFormOptions): UseFormResult {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState>>({});
const [touched, setTouched] = useState>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const validateField = useCallback(
(field: K, value: T[K]): string | null => {
const rule = validationRules[field];
return rule ? rule(value) : null;
},
[validationRules]
);
const handleChange = useCallback(
(field: K, value: T[K]) => {
setValues((prev) => ({ ...prev, [field]: value }));
if (touched[field]) {
const error = validateField(field, value);
setErrors((prev) => ({ ...prev, [field]: error ?? undefined }));
}
},
[touched, validateField]
);
const handleBlur = useCallback(
(field: K) => {
setTouched((prev) => ({ ...prev, [field]: true }));
const error = validateField(field, values[field]);
setErrors((prev) => ({ ...prev, [field]: error ?? undefined }));
},
[values, validateField]
);
const validateAll = useCallback((): boolean => {
const newErrors: Partial> = {};
let isValid = true;
for (const key of Object.keys(values) as Array) {
const error = validateField(key, values[key]);
if (error) {
newErrors[key] = error;
isValid = false;
}
}
setErrors(newErrors);
return isValid;
}, [values, validateField]);
const handleSubmit = useCallback(
async (e?: React.FormEvent) => {
e?.preventDefault();
if (!validateAll()) return;
setIsSubmitting(true);
try {
await onSubmit(values);
} finally {
setIsSubmitting(false);
}
},
[values, validateAll, onSubmit]
);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
setTouched({});
}, [initialValues]);
const setFieldValue = useCallback((field: K, value: T[K]) => {
setValues((prev) => ({ ...prev, [field]: value }));
}, []);
const setFieldError = useCallback((field: K, error: string) => {
setErrors((prev) => ({ ...prev, [field]: error }));
}, []);
const isValid = Object.keys(errors).length === 0;
return {
values,
errors,
touched,
isSubmitting,
isValid,
handleChange,
handleBlur,
handleSubmit,
reset,
setFieldValue,
setFieldError,
};
}
// Generic selection hook
function useSelection() {
const [selectedIds, setSelectedIds] = useState>(new Set());
const select = useCallback((id: T['id']) => {
setSelectedIds((prev) => new Set(prev).add(id));
}, []);
const deselect = useCallback((id: T['id']) => {
setSelectedIds((prev) => {
const next = new Set(prev);
next.delete(id);
return next;
});
}, []);
const toggle = useCallback((id: T['id']) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
const isSelected = useCallback(
(id: T['id']) => selectedIds.has(id),
[selectedIds]
);
return {
selectedIds: Array.from(selectedIds),
select,
deselect,
toggle,
isSelected,
selectedCount: selectedIds.size,
};
}
```
**Benefits:**
- Type safety without `any`: generics preserve type information throughout the hook
- Constraints using `extends` limit generic types to specific shapes
- Well-designed generics often do not need explicit type arguments
- Generic hooks work with any compatible type for maximum reusability
- Clear contracts: generic return types communicate what hooks provide
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## Event Handler Types
**Impact: HIGH (provides autocomplete for event properties and catches errors at compile time)**
Event handlers in React use synthetic events with specific types. Using the correct types provides autocomplete for event properties and catches errors at compile time.
## Incorrect
```tsx
// ❌ Bad
// Using native Event instead of React event
const handleClick = (e: Event) => {
// Missing React-specific properties
}
// Missing element type
const handleChange = (e: React.ChangeEvent) => {
e.target.value // Error: Property 'value' does not exist
}
// Wrong event type
const handleSubmit = (e: React.MouseEvent) => { // Should be FormEvent
e.preventDefault()
}
```
**Problems:**
- Native `Event` type lacks React synthetic event properties
- Missing element type generic means `target` properties are unknown
- Wrong event type does not match the actual DOM event being handled
## Correct
```tsx
// ✅ Good
// Mouse Events
const handleClick = (e: React.MouseEvent) => {
console.log(e.clientX, e.clientY)
console.log(e.currentTarget.disabled) // Button properties
}
const handleDivClick = (e: React.MouseEvent) => {
console.log(e.currentTarget.className)
}
Click
Click
// Form Events
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
const formData = new FormData(e.currentTarget)
}
const handleInputChange = (e: React.ChangeEvent) => {
console.log(e.target.value)
console.log(e.target.type)
console.log(e.target.checked) // For checkboxes
}
const handleSelectChange = (e: React.ChangeEvent) => {
console.log(e.target.value)
console.log(e.target.selectedOptions)
}
const handleTextareaChange = (e: React.ChangeEvent) => {
console.log(e.target.value)
}
// Keyboard Events
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
}
if (e.key === 'Escape') {
// close
}
console.log(e.ctrlKey, e.shiftKey, e.altKey)
}
// Focus Events
const handleFocus = (e: React.FocusEvent) => {
console.log('Focused:', e.target.name)
}
const handleBlur = (e: React.FocusEvent) => {
console.log('Blurred:', e.target.value)
}
// Drag Events
const handleDragStart = (e: React.DragEvent) => {
e.dataTransfer.setData('text/plain', 'data')
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
const data = e.dataTransfer.getData('text/plain')
}
// Inline vs Defined Handlers
// Inline - types inferred automatically
{
// e is React.MouseEvent
console.log(e.clientX)
}}>
Click
// Defined separately - must type explicitly
const handleButtonClick = (e: React.MouseEvent) => {
console.log(e.clientX)
}
Click
// Typing event handler props
interface ButtonProps {
onClick?: React.MouseEventHandler
onFocus?: React.FocusEventHandler
}
```
**Benefits:**
- Full autocomplete for event-specific properties like `clientX`, `key`, `target.value`
- Compile-time errors when accessing properties that do not exist on the event type
- Proper element type matching ensures `currentTarget` has the correct properties
- `React.MouseEventHandler` and similar aliases simplify prop typing
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## Click Event Handler Typing
**Impact: HIGH (catches wrong element types and event property access at compile time)**
Properly typing click event handlers for various React elements. Using the correct event and element types provides autocomplete for event properties and catches errors.
## Incorrect
```tsx
// ❌ Bad
// Using 'any' for event parameter
const handleClick = (e: any) => {
console.log(e.target.value);
};
// Missing event type entirely
const onClick = (e) => {
e.preventDefault();
};
// Using wrong event type
const handleButtonClick = (e: React.MouseEvent) => {
// Should be HTMLButtonElement for button clicks
};
// Not handling synthetic events properly
const handleLinkClick = (e: MouseEvent) => {
// Should be React.MouseEvent, not DOM MouseEvent
e.preventDefault();
};
```
**Problems:**
- Using `any` removes all type checking on event properties
- Missing type annotations result in implicit `any`
- Wrong element type generic means `currentTarget` has wrong properties
- Native `MouseEvent` lacks React synthetic event methods
## Correct
```tsx
// ✅ Good
import React, { useCallback } from 'react';
// Button click with proper typing
const handleButtonClick = (event: React.MouseEvent) => {
console.log('Button clicked');
console.log('Button text:', event.currentTarget.textContent);
};
// Div/container click
const handleContainerClick = (event: React.MouseEvent) => {
event.stopPropagation();
console.log('Container clicked at:', event.clientX, event.clientY);
};
// Link click with preventDefault
const handleLinkClick = (event: React.MouseEvent) => {
event.preventDefault();
const href = event.currentTarget.href;
console.log('Navigating to:', href);
};
// Generic click handler for reusable components
type ClickHandler = (
event: React.MouseEvent
) => void;
// Click with data attribute
interface ItemProps {
id: string;
name: string;
onClick: (id: string) => void;
}
function Item({ id, name, onClick }: ItemProps): React.ReactElement {
const handleClick = useCallback(
(event: React.MouseEvent) => {
event.stopPropagation();
onClick(id);
},
[id, onClick]
);
return {name} ;
}
// Click handler factory for list items
function createClickHandler(
item: T,
callback: (item: T) => void
): React.MouseEventHandler {
return (event) => {
event.stopPropagation();
callback(item);
};
}
// Component with optional click handler
interface CardProps {
title: string;
children: React.ReactNode;
onClick?: React.MouseEventHandler;
isClickable?: boolean;
}
function Card({
title,
children,
onClick,
isClickable = !!onClick,
}: CardProps): React.ReactElement {
return (
{title}
{children}
);
}
// Accessible click handler (keyboard support)
function AccessibleButton({
onClick,
children,
}: {
onClick: () => void;
children: React.ReactNode;
}): React.ReactElement {
const handleClick = (event: React.MouseEvent) => {
onClick();
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onClick();
}
};
return (
{children}
);
}
// Using MouseEventHandler type alias
type ButtonClickHandler = React.MouseEventHandler;
const myButtonHandler: ButtonClickHandler = (event) => {
console.log('Button:', event.currentTarget.name);
};
```
**Benefits:**
- Proper event types catch errors like accessing wrong properties
- Event type should match the actual HTML element
- `React.MouseEvent` provides synthetic event methods unlike native `MouseEvent`
- `currentTarget` is typed while `target` needs assertion
- `React.MouseEventHandler` simplifies function signatures
Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app)
---
## Typing Form Events
**Impact: HIGH (prevents runtime errors from untyped form data access)**
Form events require specific types for each input element. Using `any` or omitting types loses all safety around `e.target.value` and form data access.
## Incorrect
```tsx
// ❌ Bad
function LoginForm() {
const [email, setEmail] = useState("");
const [role, setRole] = useState("");
// Untyped event — no safety on e.target
const handleChange = (e) => {
setEmail(e.target.value);
};
// Using 'any' — defeats TypeScript
const handleSubmit = (e: any) => {
e.preventDefault();
console.log(e.target.email.value);
};
// Wrong event type for select
const handleSelect = (e: React.ChangeEvent) => {
setRole(e.target.value); // Should be HTMLSelectElement
};
return (
);
}
```
**Problems:**
- `any` and untyped handlers provide no autocomplete or type checking
- Wrong element types (e.g., `HTMLInputElement` for a ``) cause silent mismatches
- `e.target` is typed as `EventTarget`, not the specific element, without proper generics
## Correct
```tsx
// ✅ Good
function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [role, setRole] = useState("");
const handleEmailChange = (e: React.ChangeEvent) => {
setEmail(e.target.value); // e.target is HTMLInputElement
};
const handlePasswordChange = (e: React.ChangeEvent) => {
setPassword(e.target.value);
};
const handleRoleChange = (e: React.ChangeEvent) => {
setRole(e.target.value); // e.target is HTMLSelectElement
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
email: formData.get("email") as string,
password: formData.get("password") as string,
role: formData.get("role") as string,
};
console.log("Submitting:", data);
};
return (
);
}
```
**Benefits:**
- Each input element gets its correct event type (`HTMLInputElement`, `HTMLSelectElement`, `HTMLTextAreaElement`)
- `e.target` properties are fully typed with autocomplete
- `React.FormEvent` gives typed access to `e.currentTarget` for `FormData`
- Compile-time errors if you access properties that don't exist on the element
Reference: [React TypeScript Cheatsheet — Forms and Events](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forms_and_events)
---
## Typing Keyboard Events
**Impact: HIGH (catches invalid key checks and missing modifier handling at compile time)**
Keyboard events need specific element types and proper use of `e.key`, `e.code`, and modifier properties. Using `any` loses all safety.
## Incorrect
```tsx
// ❌ Bad
function SearchInput() {
// Using 'any' — no type safety on key properties
const handleKeyDown = (e: any) => {
if (e.key === "Enter") {
e.target.blur();
}
};
// Using native DOM KeyboardEvent instead of React's
const handleKeyUp = (e: KeyboardEvent) => {
console.log(e.key); // Wrong type — should be React.KeyboardEvent
};
// Untyped handler — e is implicitly 'any'
const handleKeyPress = (e) => {
if (e.keyCode === 13) {
// keyCode is deprecated
console.log("Enter pressed");
}
};
return ;
}
```
**Problems:**
- `any` prevents autocomplete for `key`, `code`, `metaKey`, `ctrlKey`, etc.
- Native `KeyboardEvent` is not the same as `React.KeyboardEvent` and causes type errors
- `keyCode` is deprecated; `key` and `code` are the modern standard
- No element type means `e.currentTarget` is untyped
## Correct
```tsx
// ✅ Good
function SearchInput({ onSearch }: { onSearch: (query: string) => void }) {
const handleKeyDown = (e: React.KeyboardEvent) => {
// e.key is the logical key value ("Enter", "Escape", "a", etc.)
if (e.key === "Enter") {
e.preventDefault();
onSearch(e.currentTarget.value); // currentTarget is HTMLInputElement
}
if (e.key === "Escape") {
e.currentTarget.blur();
}
};
return ;
}
// Keyboard shortcuts with modifier keys
function Editor({ onSave, onUndo }: { onSave: () => void; onUndo: () => void }) {
const handleKeyDown = (e: React.KeyboardEvent) => {
// e.metaKey = Cmd on Mac, e.ctrlKey = Ctrl on Windows/Linux
const isModifier = e.metaKey || e.ctrlKey;
if (isModifier && e.key === "s") {
e.preventDefault(); // Prevent browser save dialog
onSave();
}
if (isModifier && e.key === "z") {
e.preventDefault();
onUndo();
}
// e.code gives the physical key ("KeyS", "KeyZ", "Space")
// Useful for keyboard shortcuts that should work regardless of layout
if (e.code === "Space" && e.shiftKey) {
e.preventDefault();
console.log("Shift+Space pressed");
}
};
return ;
}
// Typed handler for non-input elements
function Modal({ onClose, children }: { onClose: () => void; children: React.ReactNode }) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
return (
{children}
);
}
```
**Benefits:**
- `React.KeyboardEvent` provides typed access to `key`, `code`, `metaKey`, `ctrlKey`, `shiftKey`, `altKey`
- `e.currentTarget` is properly typed as the specific HTML element
- Modern `e.key` and `e.code` instead of deprecated `keyCode`
- Modifier key checks enable proper cross-platform keyboard shortcuts
Reference: [React TypeScript Cheatsheet — Forms and Events](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forms_and_events)
---
## useRef for DOM Elements
**Impact: HIGH (prevents null access crashes and enables precise DOM typing)**
When using `useRef` for DOM elements, pass `null` as the initial value and use the most specific HTML element type. This returns a `RefObject` with a read-only `.current` that is `null` until React attaches the element.
## Incorrect
```tsx
// ❌ Bad
function BadForm() {
// Missing type parameter — .current is undefined, not null
const inputRef = useRef();
// Too generic — loses element-specific properties like .value, .focus()
const anotherRef = useRef(null);
// Missing null initial value — returns MutableRefObject, not RefObject
const divRef = useRef();
const handleClick = () => {
// No null check — crashes if element isn't mounted
console.log(inputRef.current.value);
inputRef.current.focus();
};
return (