Event handling
In this chapter, you learn about event handling in React.
What you'll learn in this chapter
- The basic way to write event handlers
- Specifying event types in TypeScript
- Event propagation and control
- Naming conventions for event handlers
This chapter uses the project you created in Chapter 3. Start the development server with npm run dev and study while writing code.
The basics of event handlers
In React, you specify event handlers in camelCase. *As you learned in Chapter 6, you can also pass functions as Props.
function Button() {
const handleClick = () => {
console.log('Clicked')
}
return <button onClick={handleClick}>Click</button>
}
onClick={handleClick} passes a reference to the function. If you write onClick={handleClick()}, it runs immediately during rendering, so be careful.
// OK: runs on click
<button onClick={handleClick}>
// NG: runs immediately during rendering
<button onClick={handleClick()}>
Writing inline
You can write simple processing inline.
function Button() {
return (
<button onClick={() => console.log('Click')}>
Click
</button>
)
}
The event object
An event handler receives an event object as its argument.
function Input() {
// React.ChangeEvent<HTMLInputElement> is the type of an input element's change event
// <HTMLInputElement> specifies the type of the element where the event occurred
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// Access the input element with event.target
// Get the entered value with event.target.value
console.log(event.target.value)
}
return <input onChange={handleChange} />
}
What is a SyntheticEvent?
React's event object is called a "SyntheticEvent." This is a wrapper that absorbs differences between browsers and provides a consistent API.
// React.ChangeEvent is a kind of SyntheticEvent
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// You can access the original browser event with e.nativeEvent
console.log(e.nativeEvent)
}
In most cases, the SyntheticEvent API alone is enough.
Major event types
| Event | Type | When it fires |
|---|---|---|
| onClick | React.MouseEvent | On click |
| onChange | React.ChangeEvent | On value change |
| onSubmit | React.FormEvent | On form submission |
| onKeyDown | React.KeyboardEvent | On key press |
| onFocus | React.FocusEvent | On focus |
| onBlur | React.FocusEvent | On focus loss |
Click events
function ClickExample() {
// React.MouseEvent<HTMLButtonElement> is the type of a button's mouse event
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
// Get the click position (coordinates within the browser viewport) with clientX, clientY
console.log('Click position:', event.clientX, event.clientY)
}
return <button onClick={handleClick}>Click</button>
}
Let's try it: a click counter
Create a component where the count increases each time you click the button, and "Click number N" is shown in the console.
Hint
- Manage the count with
useState(0) - Call
setCountandconsole.loginsidehandleClick - Since
countis fixed to its value at that point in the render, store the new value in a variable before using it
Answer and explanation
import { useState } from 'react'
import './App.css'
function ClickCounter() {
const [count, setCount] = useState(0)
const handleClick = () => {
// count is fixed to its value at this point in the render, so when you want to
// use the updated value, compute it first and store it in a variable
const newCount = count + 1
setCount(newCount)
// If you log count as-is, the value before the update is shown
console.log(`Click number ${newCount}`)
}
return (
<div>
<p>Click count: {count}</p>
<button onClick={handleClick}>Click</button>
</div>
)
}
export default function App() {
return <ClickCounter />
}
Since count is fixed to its value at that point in the render (a snapshot), to show the new value in the console log we store count + 1 in a variable first and then use it. This way, the display and the log always match.
Input events
function InputExample() {
const [value, setValue] = useState('')
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// Save the input field's current value into State
setValue(event.target.value)
}
return (
<div>
{/* value={value} keeps the input field's value in sync with State (a controlled component) */}
<input value={value} onChange={handleChange} />
<p>Input value: {value}</p>
</div>
)
}
Form submission events
function FormExample() {
const [name, setName] = useState('')
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value)
}
// The handler for form submission
// React.FormEvent<HTMLFormElement> is the type of a form submission event
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
// Prevent the browser's default behavior (page navigation)
// Without this, the page reloads
event.preventDefault()
console.log('Submit:', name)
}
return (
// Clicking a type="submit" button, or pressing Enter, fires onSubmit
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={handleNameChange}
/>
<button type="submit">Submit</button>
</form>
)
}
Keyboard events
function KeyboardExample() {
// React.KeyboardEvent<HTMLInputElement> is the type of a keyboard event
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
// Get the pressed key as a string with event.key
// 'Enter', 'Escape', 'ArrowUp', and so on
if (event.key === 'Enter') {
console.log('The Enter key was pressed')
}
}
return <input onKeyDown={handleKeyDown} placeholder="Press Enter to submit" />
}
Let's try it: display keyboard input
Create a component that displays the key pressed in an input field in real time.
Hint
- Get
event.keywith theonKeyDownevent - Manage the last pressed key with
useState('') - Set a default display for when there's no input with the
||operator
Answer and explanation
import { useState } from 'react'
import './App.css'
function KeyDisplay() {
const [lastKey, setLastKey] = useState('')
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
setLastKey(event.key)
}
return (
<div>
<input
onKeyDown={handleKeyDown}
placeholder="Press a key"
/>
<p>Pressed key: {lastKey || '(none)'}</p>
</div>
)
}
export default function App() {
return <KeyDisplay />
}
You can get which key was pressed with event.key. Special keys like Enter, Escape, and ArrowUp are also obtained as strings. We set a default display for when there's no input with the || operator.
Passing arguments to event handlers
When you want to pass additional arguments, wrap them in an arrow function.
function ItemList() {
const items = ['Apple', 'Banana', 'Orange']
// A handler that takes multiple arguments
const handleClick = (item: string, index: number) => {
console.log(`${index}: ${item} was clicked`)
}
return (
<ul>
{items.map((item, index) => (
<li key={item}>
{/* By wrapping in an arrow function like onClick={() => handleClick(item, index)},
you can pass arguments.
If you write onClick={handleClick(item, index)},
it runs immediately during rendering, so be careful */}
<button onClick={() => handleClick(item, index)}>
{item}
</button>
</li>
))}
</ul>
)
}
Event propagation
Event bubbling
Events propagate (bubble) from child elements to parent elements.
function BubblingExample() {
return (
<div onClick={() => console.log('Parent clicked')}>
<button onClick={() => console.log('Child clicked')}>
Click
</button>
</div>
)
// On button click:
// "Child clicked"
// "Parent clicked"
}
Stopping propagation
You can stop event propagation with stopPropagation().
function StopPropagationExample() {
return (
<div onClick={() => console.log('Parent clicked')}>
<button onClick={(e) => {
e.stopPropagation()
console.log('Child clicked')
}}>
Click
</button>
</div>
)
// On button click: only "Child clicked"
}
Let's try it: check event propagation
Set an onClick on both a parent element and a child element, and check the behavior of stopPropagation().
Hint
- Set
onClickon both the parent element (<div>) and the child element (<button>) - Call
e.stopPropagation()in the child element's handler - Comment it out to see the difference in behavior
Answer and explanation
import './App.css'
function PropagationExample() {
const handleParentClick = () => {
console.log('The parent element was clicked')
}
const handleChildClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log('The child element was clicked')
// event.stopPropagation() // uncomment to stop propagation
}
return (
<div
onClick={handleParentClick}
style={{ padding: '20px', backgroundColor: '#eee' }}
>
<p>Parent element (click here)</p>
<button onClick={handleChildClick}>
Child element (button)
</button>
</div>
)
}
export default function App() {
return <PropagationExample />
}
By default, clicking the button displays them in the order "The child element was clicked" → "The parent element was clicked". If you uncomment event.stopPropagation(), only the child element's log is shown, confirming that propagation to the parent element stops.
Preventing default behavior
You can prevent the browser's default behavior with preventDefault().
function PreventDefaultExample() {
// React.MouseEvent<HTMLAnchorElement> is the type of a link element's mouse event
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
// Prevent the browser's default behavior (page navigation) with preventDefault()
// You can similarly prevent a page reload with a form's onSubmit
event.preventDefault()
console.log('Link clicked (no navigation)')
}
return (
<a href="https://example.com" onClick={handleClick}>
Link
</a>
)
}
Naming conventions for event handlers
By convention, name event handlers starting with handle.
function Form() {
const handleSubmit = () => { /* ... */ }
const handleNameChange = () => { /* ... */ }
const handleEmailChange = () => { /* ... */ }
// When passing as a Prop, start with on
return <Button onClick={handleSubmit} />
}
Choosing between on and handle
| Place | Prefix | Example | Meaning |
|---|---|---|---|
| Props (the receiving side) | on | onClick | "on a click" |
| Function (the defining side) | handle | handleClick | "handle a click" |
// Child component: receive Props with on
function Button({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>Click</button>
}
// Parent component: define functions with handle
function App() {
const handleClick = () => {
console.log('Clicked')
}
return <Button onClick={handleClick} />
}
Summary
- Specify events in camelCase (
onClick,onChange) - Specify event types in TypeScript to handle them safely
- Prevent default behavior with
preventDefault() - Stop event propagation with
stopPropagation() - Name handlers starting with
handle
In the next chapter, you learn about conditional rendering and lists.