Skip to main content

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
info

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>
}
warning

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

EventTypeWhen it fires
onClickReact.MouseEventOn click
onChangeReact.ChangeEventOn value change
onSubmitReact.FormEventOn form submission
onKeyDownReact.KeyboardEventOn key press
onFocusReact.FocusEventOn focus
onBlurReact.FocusEventOn 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
  1. Manage the count with useState(0)
  2. Call setCount and console.log inside handleClick
  3. Since count is 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
  1. Get event.key with the onKeyDown event
  2. Manage the last pressed key with useState('')
  3. 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
  1. Set onClick on both the parent element (<div>) and the child element (<button>)
  2. Call e.stopPropagation() in the child element's handler
  3. 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

PlacePrefixExampleMeaning
Props (the receiving side)ononClick"on a click"
Function (the defining side)handlehandleClick"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.