ReactTyping Events & Refs

Typing Events & Refs

React wraps native browser events in its own SyntheticEvent system, and TypeScript provides precise types for every event and element combination. Getting event types right unlocks autocomplete for e.currentTarget, e.key, e.preventDefault(), and every other event property.

The Core Event Types

React event types follow the pattern React.EventType<HTMLElementType>. The element type determines which properties are available on e.currentTarget:

Event

Handler type

Triggered by

Change

React.ChangeEvent<HTMLInputElement>

<input>, <select>, <textarea>

Click

React.MouseEvent<HTMLButtonElement>

<button>, <div>, any clickable

Submit

React.FormEvent<HTMLFormElement>

<form onSubmit>

Keyboard

React.KeyboardEvent<HTMLInputElement>

onKeyDown, onKeyUp, onKeyPress

Focus / Blur

React.FocusEvent<HTMLInputElement>

onFocus, onBlur

Drag

React.DragEvent<HTMLDivElement>

onDragStart, onDrop, etc.

Pointer

React.PointerEvent<HTMLDivElement>

onPointerDown, onPointerMove

currentTarget vs target

This is one of the most important distinctions in React event typing. e.currentTarget is always typed to the element the handler is attached to. e.target is typed as EventTarget — the base interface with very few properties — because the event can bubble up from a child element:

TSX
function Form() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // ✓ currentTarget is HTMLInputElement — full autocomplete
    const value = e.currentTarget.value     // string
    const name  = e.currentTarget.name      // string
    const checked = e.currentTarget.checked // boolean (for checkboxes)

    // ✗ Avoid e.target for value access — it's just EventTarget
    // You'd need to cast: (e.target as HTMLInputElement).value
  }

  return <input name="email" onChange={handleChange} />
}
Tip
Always prefer `e.currentTarget` over `e.target` in React. The only time you need `e.target` is when checking which child element was clicked inside a parent's click handler — a rare pattern.
Input and Change Events

TSX
import { useState } from 'react'

function SearchBar() {
  const [query, setQuery] = useState('')

  // The event is typed to the exact input element
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.currentTarget.value)
  }

  // Clearing — functional state update, no event needed
  const handleClear = () => setQuery('')

  return (
    <div>
      <input
        type="search"
        value={query}
        onChange={handleChange}
        placeholder="Search..."
      />
      <button onClick={handleClear}>Clear</button>
      <p>Query: {query}</p>
    </div>
  )
}

// Select element — use HTMLSelectElement
function ColorPicker() {
  const [color, setColor] = useState('red')

  const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
    setColor(e.currentTarget.value)
  }

  return (
    <select value={color} onChange={handleChange}>
      <option value="red">Red</option>
      <option value="blue">Blue</option>
      <option value="green">Green</option>
    </select>
  )
}
Mouse Events

TSX
function ClickableCard() {
  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
    console.log('Clicked at:', e.clientX, e.clientY)
    console.log('Shift held:', e.shiftKey)
    console.log('Element:', e.currentTarget.id)
  }

  const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    // Stop the event from bubbling to the parent div
    e.stopPropagation()
    console.log('Button clicked')
  }

  return (
    <div id="card" onClick={handleClick}>
      <button onClick={handleButtonClick}>Inner button</button>
    </div>
  )
}

// Inline handlers are automatically typed from the element — no annotation needed
function QuickButton() {
  return (
    <button onClick={(e) => {
      // TypeScript infers React.MouseEvent<HTMLButtonElement> automatically
      e.currentTarget.blur()
    }}>
      Click me
    </button>
  )
}
Form Submit Events

TSX
import { useState, FormEvent } from 'react'

interface LoginData {
  email: string
  password: string
}

function LoginForm() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')

  // FormEvent<HTMLFormElement> is the correct type for onSubmit
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()                    // prevent page reload
    const data: LoginData = { email, password }
    console.log('Submitting:', data)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.currentTarget.value)}
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.currentTarget.value)}
      />
      <button type="submit">Log in</button>
    </form>
  )
}
Keyboard Events

TSX
function TagInput() {
  const [value, setValue] = useState('')
  const [tags, setTags] = useState<string[]>([])

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    // e.key is the modern way — don't use e.keyCode (deprecated)
    if (e.key === 'Enter' || e.key === ',') {
      e.preventDefault()
      const trimmed = value.trim()
      if (trimmed && !tags.includes(trimmed)) {
        setTags(prev => [...prev, trimmed])
      }
      setValue('')
    }
    if (e.key === 'Backspace' && value === '') {
      setTags(prev => prev.slice(0, -1))
    }
  }

  return (
    <div>
      {tags.map(tag => <span key={tag}>{tag}</span>)}
      <input
        value={value}
        onChange={(e) => setValue(e.currentTarget.value)}
        onKeyDown={handleKeyDown}
        placeholder="Add tags, press Enter"
      />
    </div>
  )
}
Focus Events

TSX
function FloatingLabel() {
  const [focused, setFocused] = useState(false)
  const [value, setValue] = useState('')

  const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
    setFocused(true)
    console.log('Focused:', e.currentTarget.name)
  }

  const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
    setFocused(false)
    // e.relatedTarget is the element receiving focus (nullable)
    console.log('Focus moved to:', e.relatedTarget)
  }

  return (
    <div className={`field ${focused || value ? 'field--active' : ''}`}>
      <input
        name="username"
        value={value}
        onFocus={handleFocus}
        onBlur={handleBlur}
        onChange={(e) => setValue(e.currentTarget.value)}
      />
      <label>Username</label>
    </div>
  )
}
Typed DOM Refs

useRef DOM refs must be typed with the exact HTML element type and initialized with null. Always null-check ref.current before accessing it:

TSX
import { useRef, useEffect, useCallback } from 'react'

function VideoPlayer({ src }: { src: string }) {
  // Type argument is the element; initial value is null
  const videoRef = useRef<HTMLVideoElement>(null)
  const containerRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    // ref.current is HTMLVideoElement | null — optional chaining handles null
    videoRef.current?.play()
  }, [])

  const handleFullscreen = useCallback(() => {
    // null check before calling DOM methods
    if (!containerRef.current) return
    containerRef.current.requestFullscreen()
  }, [])

  const handleSeek = useCallback((seconds: number) => {
    if (!videoRef.current) return
    videoRef.current.currentTime = seconds   // HTMLVideoElement property
    videoRef.current.play()
  }, [])

  return (
    <div ref={containerRef}>
      <video ref={videoRef} src={src} controls />
      <button onClick={() => handleSeek(0)}>Restart</button>
      <button onClick={handleFullscreen}>Fullscreen</button>
    </div>
  )
}
Warning
Do not access `ref.current` during render — it is only populated after the component mounts. Ref access belongs inside `useEffect`, event handlers, or `useImperativeHandle`, never in the component body during rendering.
Handler Functions as Props

When passing event handlers as props, define the function signature in the props interface rather than repeating the event type everywhere:

TSX
interface InputProps {
  value: string
  onChange: (value: string) => void    // simplified — caller gets the value, not the event
  onEnter?: () => void
}

// The component abstracts the DOM event — consumers get clean string values
function SmartInput({ value, onChange, onEnter }: InputProps) {
  return (
    <input
      value={value}
      onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange(e.currentTarget.value)}
      onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === 'Enter') onEnter?.()
      }}
    />
  )
}

// Consumer — no event types needed at this level
<SmartInput
  value={name}
  onChange={setName}
  onEnter={handleSubmit}
/>
Note
Abstracting DOM events in reusable components is a best practice. The component handles the messy `React.ChangeEvent` type internally and exposes a clean `(value: string) => void` interface to the rest of the app.