# TypeScript React Patterns - Complete Reference **Version:** 2.0.0 **Date:** March 2026 **License:** MIT ## Abstract Type-safe React with TypeScript. Contains 33 rules across 7 categories covering component typing, hooks, event handling, refs, generics, context, and utility types. Based on patterns from the TypeScript React Cheatsheet. ## References - [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) - [React + TypeScript Guide](https://react.dev/learn/typescript) - [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/) --- # Sections This file defines all sections, their ordering, impact levels, and descriptions. The section ID (in parentheses) is the filename prefix used to group rules. --- ## 1. Component Typing (comp) **Impact:** CRITICAL **Description:** Foundational patterns for typing React component props. Interface vs type for props, children typing with ReactNode, default props with destructuring, forwardRef, polymorphic "as" prop, FC vs function declaration, and rest props spreading. ## 2. Hook Typing (hook) **Impact:** CRITICAL **Description:** Type-safe React hooks. useState with generic types, useRef for DOM elements and mutable values, useReducer with discriminated union actions, useCallback/useMemo with typed parameters, useContext with null checking, and custom hooks with proper return types. ## 3. Event Handling (event) **Impact:** HIGH **Description:** Typing React event handlers correctly. FormEvent, ChangeEvent, MouseEvent, KeyboardEvent with proper HTML element generics, and event handler prop types. ## 4. Ref Typing (ref) **Impact:** HIGH **Description:** TypeScript patterns for React refs. useRef with specific HTMLElement types, callback refs for DOM measurement, and useImperativeHandle for exposing component methods to parents. ## 5. Generic Components (generic) **Impact:** MEDIUM **Description:** Building reusable, type-safe generic components. Generic list, select, and table components with type inference, and generic constraints using extends and keyof. ## 6. Context & State (ctx) **Impact:** MEDIUM **Description:** Typed React Context patterns. Creating context with null default and custom hook that throws on missing provider, and combining Context with useReducer using discriminated union actions. ## 7. Utility Types (util) **Impact:** LOW **Description:** TypeScript utility types for React. ComponentPropsWithoutRef for inheriting HTML attributes, Pick/Omit/Partial for deriving prop types, and discriminated unions for modeling state machines with exhaustive checking. --- ## Component Props Interface **Impact: CRITICAL (ensures consistent and extensible component APIs)** Consistent typing strategy makes code predictable and maintainable. Interfaces are preferred for props because they are extendable, provide better error messages, and align with React's composition model. ## Incorrect ```tsx // ❌ Bad // Using type when interface is better type ButtonProps = { label: string onClick: () => void } // Inline types - not reusable function Button({ label, onClick }: { label: string; onClick: () => void }) { return } // No typing function Card(props) { return
{props.title}
} ``` **Problems:** - Type aliases cannot be extended with `extends` for composition - Inline types are not reusable across components - Missing type annotations result in implicit `any` types ## Correct ```tsx // ✅ Good // Interface for component props interface ButtonProps { label: string onClick: () => void disabled?: boolean } function Button({ label, onClick, disabled = false }: ButtonProps) { return ( ) } // Extending interfaces interface IconButtonProps extends ButtonProps { icon: React.ReactNode iconPosition?: 'left' | 'right' } function IconButton({ label, onClick, disabled, icon, iconPosition = 'left', }: IconButtonProps) { return ( ) } // Extend native button props (separate component example) interface NativeButtonProps extends React.ButtonHTMLAttributes { variant: 'primary' | 'secondary' isLoading?: boolean } function NativeButton({ variant, isLoading, children, disabled, ...props }: NativeButtonProps) { return ( ) } // Generic interface interface SelectProps { options: T[] value: T onChange: (value: T) => void getLabel: (option: T) => string } function Select({ options, value, onChange, getLabel }: SelectProps) { return ( ) } ``` **Benefits:** - Interfaces are extendable with `extends` for composable component APIs - Better TypeScript error messages compared to type aliases - Extending HTML element attributes gives components native prop support - Generic interfaces enable type-safe reusable components Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) --- ## Component Children Types **Impact: CRITICAL (prevents runtime errors from invalid children)** `children` is one of the most commonly used props. Using the wrong type causes type errors or allows invalid usage. Choose the right type based on what your component accepts. ## Incorrect ```tsx // ❌ Bad // Too restrictive - won't accept strings or numbers interface CardProps { children: React.ReactElement } // This fails: Hello // Error: string is not ReactElement {42} // Error: number is not ReactElement // No type - any is implied interface CardProps { children: any } // JSX.Element - React Native incompatible interface CardProps { children: JSX.Element } ``` **Problems:** - `React.ReactElement` rejects valid renderable values like strings, numbers, and fragments - Using `any` removes all type checking and allows invalid children - `JSX.Element` is not cross-platform compatible ## Correct ```tsx // ✅ Good // ReactNode (Most Common) - Accepts anything React can render interface CardProps { title: string children: React.ReactNode } function Card({ title, children }: CardProps) { return (

{title}

{children}
) } // All valid: Hello {42} {items.map(i => )} {showContent && } {null} // ReactElement (JSX Only) - When you need to access element props interface TabsProps { children: React.ReactElement | React.ReactElement[] } interface TabProps { label: string children: React.ReactNode } function Tabs({ children }: TabsProps) { const tabs = React.Children.toArray(children) as React.ReactElement[] return (
{tabs.map((tab, i) => ( ))}
{children}
) } // Render Props - Function as children interface DataFetcherProps { url: string children: (data: T, loading: boolean, error: Error | null) => React.ReactNode } function DataFetcher({ url, children }: DataFetcherProps) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // fetch logic... return <>{children(data as T, loading, error)} } // PropsWithChildren Utility import { PropsWithChildren } from 'react' interface CardBaseProps { title: string } function Card({ title, children }: PropsWithChildren) { return (

{title}

{children}
) } // Required vs Optional Children (pick one per component) // Option A: Required children // interface ContainerProps { children: React.ReactNode } // Option B: Truly required (must provide content) // interface ContainerProps { children: NonNullable } // Option C: Optional children // interface ContainerProps { children?: React.ReactNode } ``` **Benefits:** - Correct types prevent runtime errors - Better autocomplete and documentation - Catches invalid children at compile time Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) --- ## Default Props Typing **Impact: CRITICAL (avoids deprecated patterns and ensures type-aware defaults)** Modern approaches to typing default props in React with TypeScript use destructuring defaults instead of the deprecated `defaultProps` static property. ## Incorrect ```tsx // ❌ Bad // Using deprecated defaultProps static property interface ButtonProps { variant?: 'primary' | 'secondary'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; } const Button: React.FC = ({ variant, size, disabled, children }) => { return ( ); }; // This pattern is deprecated and will be removed Button.defaultProps = { variant: 'primary', size: 'md', disabled: false, }; // Default values not reflected in type system interface CardProps { elevation: number; rounded: boolean; } function Card({ elevation, rounded }: CardProps) { return
; } Card.defaultProps = { elevation: 2, rounded: true, }; ``` **Problems:** - `defaultProps` is deprecated for function components and will be removed - TypeScript does not reflect `defaultProps` values in the type system - Bundlers cannot tree-shake `defaultProps` effectively ## Correct ```tsx // ✅ Good // Using default parameters in destructuring (preferred modern approach) interface ButtonProps { variant?: 'primary' | 'secondary'; size?: 'sm' | 'md' | 'lg'; disabled?: boolean; children: React.ReactNode; } function Button({ variant = 'primary', size = 'md', disabled = false, children, }: ButtonProps): React.ReactElement { return ( ); } // Extracting defaults for reuse and testing interface CardProps { elevation?: number; rounded?: boolean; children: React.ReactNode; } const cardDefaults: Required> = { elevation: 2, rounded: true, }; function Card({ elevation = cardDefaults.elevation, rounded = cardDefaults.rounded, children, }: CardProps): React.ReactElement { return (
{children}
); } // Complex default objects with proper typing interface FormFieldProps { name: string; label?: string; validation?: { required?: boolean; minLength?: number; maxLength?: number; pattern?: RegExp; }; styles?: { container?: React.CSSProperties; label?: React.CSSProperties; input?: React.CSSProperties; }; } const defaultValidation: Required> = { required: false, minLength: 0, maxLength: Infinity, pattern: /.*/, }; const defaultStyles: Required> = { container: {}, label: {}, input: {}, }; function FormField({ name, label = name, validation = {}, styles = {}, }: FormFieldProps): React.ReactElement { const mergedValidation = { ...defaultValidation, ...validation }; const mergedStyles = { ...defaultStyles, ...styles }; return (
); } // Using satisfies for type-safe defaults interface ThemeProps { colors?: { primary?: string; secondary?: string; background?: string; }; spacing?: { small?: number; medium?: number; large?: number; }; } const themeDefaults = { colors: { primary: '#007bff', secondary: '#6c757d', background: '#ffffff', }, spacing: { small: 8, medium: 16, large: 24, }, } satisfies Required<{ colors: Required>; spacing: Required>; }>; type Theme = { colors: Required>; spacing: Required>; }; // import { createContext } from 'react' const defaultTheme: Theme = themeDefaults; const ThemeContext = createContext(defaultTheme); function ThemeProvider({ colors = themeDefaults.colors, spacing = themeDefaults.spacing, children, }: ThemeProps & { children: React.ReactNode }): React.ReactElement { const theme = { colors: { ...themeDefaults.colors, ...colors }, spacing: { ...themeDefaults.spacing, ...spacing }, }; return ( {children} ); } ``` **Benefits:** - Default parameters are type-aware and TypeScript understands the values inside the function - No deprecation concerns since `defaultProps` is deprecated for function components - Better tree-shaking by bundlers - Explicit defaults make component behavior clear when reading the code - Extracted default objects can be imported in tests Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) --- ## ForwardRef Typing **Impact: CRITICAL (ensures ref types match actual DOM elements)** Properly typing components that forward refs to child elements. Mismatched ref types cause runtime errors or require unsafe type assertions. ## Incorrect ```tsx // ❌ Bad // Missing ref type annotation const Input = React.forwardRef((props, ref) => { return ; }); // Incorrect ref type interface ButtonProps { variant: 'primary' | 'secondary'; } const Button = React.forwardRef((props, ref) => { // Ref type doesn't match the actual element return ; }); // Not exposing ref at all for imperative handle const ComplexInput = React.forwardRef((props: InputProps, ref) => { const inputRef = React.useRef(null); // ref is ignored, parent can't access input return ; }); ``` **Problems:** - Missing type annotations result in `any` types for ref and props - Mismatched ref element types require unsafe `as any` casts - Ignoring the forwarded ref makes the component unusable by parent components ## Correct ```tsx // ✅ Good import React, { forwardRef, useRef, useImperativeHandle } from 'react'; // Basic forwardRef with proper typing interface InputProps extends Omit, 'size'> { label: string; error?: string; size?: 'sm' | 'md' | 'lg'; } const Input = forwardRef( ({ label, error, size = 'md', className, ...rest }, ref) => { return (
{error && {error}}
); } ); Input.displayName = 'Input'; // ForwardRef with useImperativeHandle for custom methods interface FormInputHandle { focus: () => void; clear: () => void; getValue: () => string; validate: () => boolean; } interface FormInputProps { name: string; label: string; required?: boolean; pattern?: RegExp; } const FormInput = forwardRef( ({ name, label, required = false, pattern }, ref) => { const inputRef = useRef(null); const [error, setError] = React.useState(null); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current?.focus(); }, clear: () => { if (inputRef.current) { inputRef.current.value = ''; setError(null); } }, getValue: () => { return inputRef.current?.value ?? ''; }, validate: () => { const value = inputRef.current?.value ?? ''; if (required && !value) { setError('This field is required'); return false; } if (pattern && !pattern.test(value)) { setError('Invalid format'); return false; } setError(null); return true; }, })); return (
{error && {error}}
); } ); FormInput.displayName = 'FormInput'; // Generic forwardRef component interface SelectOption { value: T; label: string; } interface SelectProps { options: SelectOption[]; value?: T; onChange: (value: T) => void; placeholder?: string; } type GenericForwardRefComponent = ( props: SelectProps & { ref?: React.ForwardedRef } ) => React.ReactElement; const Select: GenericForwardRefComponent = forwardRef( ( { options, value, onChange, placeholder }: SelectProps, ref: React.ForwardedRef ) => { return ( ); } ) as GenericForwardRefComponent; ``` **Benefits:** - Proper typing ensures ref type matches the actual DOM element - `useImperativeHandle` enables custom methods with full type safety - Setting displayName improves React DevTools debugging - Special patterns enable generic forwardRef components - Parent components can imperatively control children safely Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) --- ## Polymorphic Component Typing **Impact: CRITICAL (enables type-safe rendering as different HTML elements)** Creating type-safe polymorphic components that can render as different HTML elements using the `as` prop pattern. ## Incorrect ```tsx // ❌ Bad // Using 'any' loses all type safety interface BoxProps { as?: any; children?: React.ReactNode; } const Box = ({ as: Component = 'div', ...props }: BoxProps) => { return ; }; // No validation of props for the rendered element This should have href // No type error, but missing required href // String union doesn't provide prop inference interface TextProps { as?: 'h1' | 'h2' | 'h3' | 'p' | 'span'; children: React.ReactNode; } const Text = ({ as: Component = 'p', children }: TextProps) => { return {children}; }; // Can't pass element-specific props like htmlFor to label ``` **Problems:** - Using `any` for the `as` prop removes all type safety and autocomplete - No validation that passed props match the rendered element type - String unions cannot infer element-specific props ## Correct ```tsx // ✅ Good import React from 'react'; // Core polymorphic types type AsProp = { as?: C; }; type PropsToOmit = keyof (AsProp & P); type PolymorphicComponentProps< C extends React.ElementType, Props = object > = Props & AsProp & Omit, PropsToOmit>; type PolymorphicComponentPropsWithRef< C extends React.ElementType, Props = object > = PolymorphicComponentProps & { ref?: PolymorphicRef; }; type PolymorphicRef = React.ComponentPropsWithRef['ref']; // Simple polymorphic Box component interface BoxOwnProps { padding?: 'none' | 'sm' | 'md' | 'lg'; margin?: 'none' | 'sm' | 'md' | 'lg'; display?: 'block' | 'flex' | 'grid' | 'inline'; } type BoxProps = PolymorphicComponentProps; function Box({ as, padding = 'none', margin = 'none', display = 'block', className, style, ...rest }: BoxProps): React.ReactElement { const Component = as ?? 'div'; const computedStyle: React.CSSProperties = { padding: padding !== 'none' ? `var(--spacing-${padding})` : undefined, margin: margin !== 'none' ? `var(--spacing-${margin})` : undefined, display, ...style, }; return ; } // Usage with full type inference Default div Section element Link with href autocomplete {}}>Button with onClick // Polymorphic with discriminated unions type ButtonVariant = 'solid' | 'outline' | 'ghost'; interface ButtonBaseProps { variant?: ButtonVariant; size?: 'sm' | 'md' | 'lg'; isLoading?: boolean; } type ButtonAsButton = ButtonBaseProps & Omit, keyof ButtonBaseProps> & { as?: 'button'; }; type ButtonAsLink = ButtonBaseProps & Omit, keyof ButtonBaseProps> & { as: 'a'; }; type ButtonProps = ButtonAsButton | ButtonAsLink; function Button(props: ButtonProps): React.ReactElement { const { as = 'button', variant = 'solid', size = 'md', isLoading = false, className, children, ...rest } = props; const classes = `btn btn-${variant} btn-${size} ${className ?? ''}`; if (as === 'a') { return ( )}> {isLoading ? 'Loading...' : children} ); } return ( ); } // Usage with proper type narrowing ``` **Benefits:** - Full type safety: props are validated based on the rendered element - IDE autocomplete suggests valid props for each element type - Single component can render as any HTML element or custom component - Properly typed refs match the rendered element - Reduces need for wrapper components like LinkButton, SubmitButton, etc. Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) --- ## FC vs Function Declaration **Impact: CRITICAL (ensures consistent and future-proof component typing)** Choosing between `React.FC` and regular function declarations for component typing. Regular function declarations are preferred over `React.FC` for clarity and generic support. ## Incorrect ```tsx // ❌ Bad import React, { FC } from 'react'; interface ButtonProps { label: string; onClick: () => void; } // FC used to include children implicitly (React 17 and earlier) // In React 18+, FC no longer includes children automatically const Button: FC = ({ label, onClick }) => { return ; }; // FC makes it harder to use generics const List: FC<{ items: string[] }> = ({ items }) => (
    {items.map((item) => (
  • {item}
  • ))}
); ``` **Problems:** - `React.FC` had implicit children in React 17, causing confusion across versions - `React.FC` makes generic components awkward to type - `React.FC` had issues with `defaultProps` typing - Inconsistent community conventions between React 17 and 18 ## Correct ```tsx // ✅ Good // Using regular function declarations with explicit return types interface ButtonProps { label: string; onClick: () => void; } function Button({ label, onClick }: ButtonProps): React.ReactElement { return ; } // Arrow function with explicit typing const Card = ({ title, children }: { title: string; children: React.ReactNode; }): React.ReactElement => { return (

{title}

{children}
); }; // Generic components are cleaner without FC interface ListProps { items: T[]; renderItem: (item: T) => React.ReactNode; } function List({ items, renderItem }: ListProps): React.ReactElement { return
    {items.map(renderItem)}
; } // Named function export for better debugging interface User { id: string; name: string; email: string } export function UserProfile({ user }: { user: User }): React.ReactElement { return
{user.name}
; } ``` **Benefits:** - Explicit over implicit: regular functions require explicit children prop declaration - Generic support: regular functions work better with TypeScript generics - Consistency: avoids confusion about React 17 vs 18 FC behavior - Better debugging: named function declarations appear with their names in DevTools - Industry trend: the React and TypeScript communities have moved away from FC Reference: [React TypeScript Cheatsheet](https://react-typescript-cheatsheet.netlify.app) --- ## Display Name Pattern **Impact: CRITICAL (improves debugging and DevTools component identification)** Setting displayName for better debugging and DevTools integration. Components without displayName show as "Anonymous" in React DevTools, making debugging difficult. ## Incorrect ```tsx // ❌ Bad // Anonymous arrow function - shows as "Anonymous" in DevTools export const Button = ({ children }: { children: React.ReactNode }) => { return ; }; // HOC without displayName - hard to identify wrapped component function withLogger

(Component: React.ComponentType

) { return (props: P) => { console.log('Rendering:', props); return ; }; } // Memo without displayName const ExpensiveList = React.memo(({ items }: { items: string[] }) => { return (

    {items.map((item) => (
  • {item}
  • ))}
); }); // ForwardRef without displayName const Input = React.forwardRef>( (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 ; } // Arrow function with explicit displayName export const IconButton = ({ icon, label }: { icon: React.ReactNode; label: string }): React.ReactElement => { return ( ); }; 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 ); } // 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 (

{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 ; } // 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 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}

); } // 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
{/* form fields */}
; } // 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 (

Count: {count}

); } // 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 ; }); 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 (

Count: {count}

); } ``` **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}

); } // 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
// 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 // Defined separately - must type explicitly const handleButtonClick = (e: React.MouseEvent) => { console.log(e.clientX) } // 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 `