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 |
|
|
Click |
|
|
Submit |
|
|
Keyboard |
|
|
Focus / Blur |
|
|
Drag |
|
|
Pointer |
|
|
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:
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} />
}Input and Change Events
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
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
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
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
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:
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>
)
}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:
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}
/>