Blog

Part 6 · Handling Events in React: Clicks, Typing and Forms

Make a page respond to clicks, typing and forms. Learn why you pass a function and don’t call it, how events bubble up, and when to use preventDefault.

Part 4 introduced event handlers. A button changed a number with useState, and you saw the “Too many re-renders” error. This part covers events properly. It also explains the arrow function in onClick={() => setClicks(clicks + 1)}, which Part 2 used without explaining.

An event is something that happens on the page. A click is an event. So is a key press, typing a letter, or sending a form. An event handler is a function you give React. React runs it when the event happens.

We’ll cover clicks, typing and forms. We’ll also see how one click can reach more than one handler, and some common first mistakes.

Try this first

Read this code. Don’t press Run yet.

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  function handleClick() {
    console.log('handleClick runs')
    setCount(count + 1)
  }

  return (
    <div>
      <button onClick={handleClick}>Add one</button>
      <p>Count: {count}</p>
    </div>
  )
}
handleClick runs
handleClick runs

The console.log line is inside App. Make a guess. When you press Run, before you click anything, does the Console show handleClick runs?

Now press Run. The Console is empty. Click the button two times. The Console shows the line two times, once for each click.

App ran when you pressed Run. It even ran twice, because Strict Mode is on (Part 2 explained this). But running App only made the handleClick function. It didn’t call it. React called it later, once for each click.

Notice that each click logged only one line. As Part 4 showed, Strict Mode runs your components twice in development, but not your event handlers.

Pass the function, don’t call it

Look at this line again:

<button onClick={handleClick}>Add one</button>

onClick is a prop of the <button>. Its value is the function handleClick itself. React keeps the function. When the user clicks, React calls it.

There are no () after handleClick. That matters a lot. Here is the same button with ():

export default function App() {
  function handleClick() {
    console.log('Clicked!')
  }

  return <button onClick={handleClick()}>Click me</button>
}

TypeScript stops you here. It says Type 'void' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'. In plain words: onClick wants a function, and you gave it nothing.

The playground doesn’t check types, so press Run anyway. The Console shows “Clicked!” two times, and you haven’t clicked yet. Then click the button. Nothing new appears.

Here is why. In JSX, the code inside { } runs when the component runs (you saw this in Part 1). So handleClick() runs while React is rendering App. It logs “Clicked!”. It gives back nothing, which is undefined. So the button gets onClick={undefined}, and a click does nothing.

We checked this with React 19.3. With Strict Mode on, handleClick ran 2 times when the page loaded. After 3 clicks it had still run 2 times. With the () taken out, it ran 0 times on load and 3 times after 3 clicks.

It gets worse with state

You saw this error in Part 4. Now put a state change in that handler:

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  function handleClick() {
    setCount(count + 1)
  }

  return <button onClick={handleClick()}>Count: {count}</button>
}

Press Run. React stops with this error:

Too many re-renders. React limits the number of renders to prevent an infinite loop.

Follow the steps. React renders App. That calls handleClick(), which calls setCount. Setting state asks React to render App again. That calls handleClick() again, which sets state again. This would never end, so React stops it and shows the error.

The fix for both: take out the (). Write onClick={handleClick}.

Arrow functions in JSX

Often the handler is one short line. Then you can write the function right inside the JSX:

import { useState } from 'react'

export default function App() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Add one</button>
      <p>Count: {count}</p>
    </div>
  )
}

() => setCount(count + 1) is an arrow function. It is a short way to write a function. Read it like this: “a function with no inputs. When it’s called, it runs setCount(count + 1).”

So the arrow makes a new function, and nothing runs yet. It works the same as this longer version:

function handleClick() {
  setCount(count + 1)
}

Compare these two:

  • onClick={() => setCount(count + 1)} passes a function. React calls it on a click.
  • onClick={setCount(count + 1)} calls setCount while rendering. That is the “Too many re-renders” error again.

React’s docs give the rule in one line: “Event handlers must be passed, not called!”

Passing a value to a handler

Arrow functions help when a handler needs a value. Suppose you have one function and three buttons:

import { useState } from 'react'

export default function App() {
  const [picked, setPicked] = useState('nothing')

  function handlePick(fruit: string) {
    setPicked(fruit)
  }

  return (
    <div>
      <button onClick={() => handlePick('tea')}>Tea</button>
      <button onClick={() => handlePick('milk')}>Milk</button>
      <button onClick={() => handlePick('juice')}>Juice</button>
      <p>You picked: {picked}</p>
    </div>
  )
}

Each button gets its own small arrow function. When you click “Milk”, that arrow calls handlePick('milk').

You can’t write onClick={handlePick('milk')}. That calls handlePick during rendering, the same mistake as before.

Names: handleX and onX

React code follows two naming habits. React’s docs use both.

  • A function that handles an event is named handle plus the event: handleClick, handleChange, handleSubmit.
  • A prop that takes an event handler is named on plus a capital letter: onClick, onChange, onPick.

Built-in tags like <button> and <input> only know the browser’s event names, like onClick. Your own components can use any name you like.

How a child tells its parent something happened

In Part 3 you saw that a parent passes data down to a child with props. A function is a value too. So a parent can pass a function down. The child calls it when something happens. That is how a child “talks” to its parent.

import { useState } from 'react'

function ColorButton({ color, onPick }: { color: string; onPick: (color: string) => void }) {
  return <button onClick={() => onPick(color)}>{color}</button>
}

export default function App() {
  const [picked, setPicked] = useState('nothing')

  function handlePick(color: string) {
    setPicked(color)
  }

  return (
    <div>
      <ColorButton color="Red" onPick={handlePick} />
      <ColorButton color="Blue" onPick={handlePick} />
      <p>You picked: {picked}</p>
    </div>
  )
}

Run it and click “Blue”.

  1. The <button> inside the second ColorButton gets the click.
  2. Its arrow function calls onPick('Blue').
  3. onPick is the handlePick function that App passed down. So App‘s code runs.
  4. handlePick sets App‘s state, and the page shows “You picked: Blue”.

ColorButton doesn’t know what App will do with the color. It only says “this color was picked”. App decides what that means.

The type (color: string) => void says “a function that takes a string and gives back nothing”. TypeScript then checks that App passes the right kind of function.

The event object

When React calls your handler, it passes one value to it: an event object. It holds facts about what happened. By habit, people name it e.

import { useState } from 'react'

export default function App() {
  const [info, setInfo] = useState('Click a button.')

  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    setInfo(`You clicked "${e.currentTarget.textContent}". The event type is ${e.type}.`)
  }

  return (
    <div>
      <button onClick={handleClick}>Save</button>
      <button onClick={handleClick}>Delete</button>
      <p>{info}</p>
    </div>
  )
}

e.type is the kind of event, here "click". e.currentTarget is the element whose handler is running, here the button you clicked. The two buttons share one handler, and e.currentTarget tells them apart.

Types for the event object

In TypeScript, you name the event’s type when the handler is its own function. These are the ones you’ll use most:

Event Type of e
a click on a button React.MouseEvent<HTMLButtonElement>
typing in an input React.ChangeEvent<HTMLInputElement>
a key press in an input React.KeyboardEvent<HTMLInputElement>
sending a form React.SubmitEvent<HTMLFormElement>

The part in < > is the kind of HTML element the handler is on. It tells TypeScript what e.currentTarget is.

In older code and lessons you will often see React.FormEvent for forms. React 19.3’s types mark it as deprecated, which means “old, don’t use it in new code”. Their note says “FormEvent doesn’t actually exist”. React.SubmitEvent was added in @types/react 19.2.10, in January 2026. Older code uses FormEvent because SubmitEvent didn’t exist yet. If TypeScript says SubmitEvent is missing, update @types/react.

When the arrow function is written right inside the JSX, you don’t need the type. TypeScript works it out from the prop. In <input onChange={(e) => ...} />, it knows e is a change event on an input.

What e really is

The object is not the browser’s own event. React makes its own object and puts the browser’s event inside it. React’s docs call it a “synthetic event”. Synthetic means “made”, not the original.

We checked in React 19.3. In a click handler, e instanceof MouseEvent was false. But e.nativeEvent instanceof MouseEvent was true. e.nativeEvent is the browser’s own event, if you ever need it.

React’s object has the same main parts as the browser’s: type, target, currentTarget, preventDefault() and stopPropagation(). React’s docs say it follows the same standard as the browser’s events. They also say it fixes some places where browsers behave differently.

Typing: onChange

Here is a text box that shows what you type:

import { useState } from 'react'

export default function App() {
  const [name, setName] = useState('')

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    console.log('onChange:', e.target.value)
    setName(e.target.value)
  }

  return (
    <div>
      <input aria-label="Your name" value={name} onChange={handleChange} />
      <p>Hello, {name}!</p>
    </div>
  )
}
onChange: A
onChange: An
onChange: Ana

Run it and type “Ana”. The Console shows one line for each letter.

e.target is the input box. e.target.value is the text in the box right now. The handler saves it in state, and React shows it in the paragraph.

In React, onChange runs each time the text in the box changes. So it usually runs on every key you type. Keys that don’t change the text, like Shift or the arrow keys, don’t run it. Pasting text does. React’s docs say it works like the browser’s own input event.

This is different from plain HTML. A browser also has its own change event. MDN is a set of web guides that many developers use. It says that for a text box, change fires when the box loses focus after its value changed. React’s onChange doesn’t wait for that.

The box gets its value from state (value={name}), and every key goes through onChange back into state. An input like this is called a controlled input. Part 28 explains controlled inputs fully.

Forms: onSubmit and preventDefault

A form groups inputs with a button that sends them. Sending a form is called submitting it.

import { useState } from 'react'

export default function App() {
  const [name, setName] = useState('')
  const [message, setMessage] = useState('')

  function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
    e.preventDefault()
    setMessage(`Saved: ${name}`)
    setName('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
      <button type="submit">Save</button>
      <p>{message}</p>
    </form>
  )
}

Type a name and click “Save”. The message appears, and the box is empty again.

Three things to notice:

  • The handler goes on the <form>, as onSubmit. It doesn’t go on the button.
  • Clicking a <button type="submit"> inside the form submits it. A <button> inside a form with no type does too. We checked: the form’s onSubmit ran for both.
  • That is a trap. Say you add a “Clear” button to the form. With no type, it submits the form too. Write <button type="button"> for any button that should not submit. We checked: a type="button" click didn’t run onSubmit.
  • Pressing Enter in a text box usually submits the form too. The HTML standard covers this case. It says the browser must then act as if the form’s first submit button was clicked. So one onSubmit handles both the click and the Enter key.

What e.preventDefault() stops

Some events come with something the browser does on its own. That is called the default action. For a form, React’s <form> page says the browser sends the form’s data and loads the page again.

Loading the page again would clear your app’s state. The message would never stay on the page. e.preventDefault() tells the browser: “don’t do your default action. My code handles this.”

We checked in React 19.3: after e.preventDefault(), the event’s e.defaultPrevented was true.

React 19 also lets you pass a function to a form’s action prop instead. Part 29 covers form Actions. onSubmit with e.preventDefault() works in every version of React, so it’s the place to start.

Events bubble up

Here are three tags inside each other, and each one has a click handler:

export default function App() {
  return (
    <section onClick={() => console.log('section onClick')}>
      <div onClick={() => console.log('div onClick')}>
        <button onClick={() => console.log('button onClick')}>Click me</button>
      </div>
    </section>
  )
}
button onClick
div onClick
section onClick

Make a guess before you press Run. You click the button. Which handlers run, and in what order?

All three run. The button’s handler runs first, then the div’s, then the section’s.

1. You click the button. The browser makes one click event. 2. React runs the button's onClick first. 3. The event bubbles up to the div. Its onClick runs next. 4. Then up to the section. Its onClick runs last. <section onClick> <div onClick> <button onClick> Console 1. button onClick 2. div onClick 3. section onClick One click, three handlers, from the inside out.

An event bubbling up. Press play, or step through it with the arrows.

The same steps in words:

  1. You click the button. The browser makes one click event.
  2. React runs the button’s onClick first.
  3. The event moves up to the parent, the <div>. Its onClick runs next.
  4. Then up to the <section>. Its onClick runs last.

This is called bubbling, or propagation. The event starts where it happened, then goes up through every parent, like a bubble rising in water. Any parent with a handler for that event gets to run it.

If you click the <div> itself, outside the button, only the div’s and the section’s handlers run. The event starts at the div, so the button never sees it. React’s docs say that all events bubble in React except onScroll. onMouseEnter and onMouseLeave are also different. They don’t bubble up through the parents. React’s docs say they go from the element the mouse leaves to the one it enters.

Stopping it: e.stopPropagation()

Sometimes a click on a button should not count as a click on the box around it. Then call e.stopPropagation() in the button’s handler:

export default function App() {
  return (
    <section onClick={() => console.log('section onClick')}>
      <div onClick={() => console.log('div onClick')}>
        <button
          onClick={(e) => {
            console.log('button onClick')
            e.stopPropagation()
          }}
        >
          Click me
        </button>
      </div>
    </section>
  )
}
button onClick

Now only the button’s handler runs. The event stops at the button and never reaches the div or the section.

Don’t mix up the two methods. They do different jobs:

  • e.stopPropagation() stops the event from reaching the parents’ handlers.
  • e.preventDefault() stops the browser’s default action, like loading the page again.

The capture phase: onClickCapture

There is one more step, and you’ll rarely need it. Before the event bubbles up, it first travels down from the top to the element you clicked. Handlers named with Capture at the end run on the way down.

We added onClickCapture to the section and the div in the bubbling example, and clicked the button. With React 19.3, the order was:

section onClickCapture
div onClickCapture
button onClick
div onClick
section onClick

Down through the capture handlers, then up through the normal ones. When the button called e.stopPropagation(), the order was the two capture handlers, then button onClick. The capture handlers had already run on the way down, before the button stopped the event.

It works the other way too. A capture handler higher up can stop the event before it reaches the button. We made the div’s onClickCapture call e.stopPropagation(). Then only div onClickCapture ran. The button’s onClick never ran. They help code that must see every click, even the stopped ones. In most app code you won’t need them.

An everyday example

Think of a letter sent up through an office. A worker gets the letter first and reads it. Then they pass it to their team leader, who reads it too. Then it goes to the head of the office. Each person may act on it. stopPropagation() is the worker saying “this stops here”. The letter never goes up.

The exact version

In the office, each person gets the letter in turn. React works a little differently. It doesn’t put a listener on each tag. A listener is the browser’s word for a function waiting for an event. React listens near the top of the page instead. When a click arrives there, React starts at the clicked element and walks up through its parents. It collects each onClick it finds on the way. Then it calls them, one after another. We read this in React 19.3’s own code.

Since React 17, React listens for events at the root container. That is the one HTML element you gave to createRoot, where your whole app lives. React’s blog post for version 17 says: “React 17 will call rootNode.addEventListener()“. Before that, React listened on the whole document. Listening high up for many elements at once is called event delegation.

We checked with React 19.3. Making a root added 142 listeners to the root container, for 87 kinds of events. It also added one listener to document, for an event called selectionchange. Then we rendered a section, a div and a button with onClick. React added no listeners to those three elements at all.

So the browser’s own currentTarget is the root, because that is where the listener is. We checked: inside a button’s onClick, e.nativeEvent.currentTarget was the root container. React’s docs say React’s event object holds “the values your React code expects” instead. That is why e.currentTarget is your button, not the root.

Keyboard and focus, briefly

onKeyDown runs when a key goes down. e.key says which key it was, as text: "a", "Enter", "Escape", "ArrowUp".

An element has focus when it’s the one that gets your key presses. A text box gets focus when you click into it or move to it with the Tab key. onFocus runs when it gets focus. onBlur runs when it loses focus. Blur is the word browsers use for losing focus.

import { useState } from 'react'

export default function App() {
  const [lastKey, setLastKey] = useState('none')
  const [focused, setFocused] = useState(false)

  return (
    <div>
      <input
        aria-label="Type here"
        onKeyDown={(e) => setLastKey(e.key)}
        onFocus={() => setFocused(true)}
        onBlur={() => setFocused(false)}
      />
      <p>Last key: {lastKey}</p>
      <p>The box is {focused ? 'focused' : 'not focused'}.</p>
    </div>
  )
}

Click in the box and press some keys. Then click outside the box.

In React, focus events bubble too. React’s docs say so. We checked: a <div> around the input got onFocus and onBlur when the input did.

Common mistakes

Calling the handler instead of passing it

onClick={handleClick()} runs handleClick while rendering, not on a click. If it sets state, React stops with “Too many re-renders”. The fix: onClick={handleClick}. If you need to pass a value, use an arrow: onClick={() => handlePick('milk')}.

Forgetting e.preventDefault() on a form

import { useState } from 'react'

export default function App() {
  const [sent, setSent] = useState(0)

  function handleSubmit() {
    console.log('Sending...')
    setSent(sent + 1)
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>Times sent: {sent}</p>
      <button type="submit">Send</button>
    </form>
  )
}

Press Run, then click “Send” a few times. After each click, the box shows “Times sent: 0” again.

Your handler runs, and then the browser does its default action. It sends the form and loads the page again. In the playground, the example’s box loads again. Your code runs from the start, so sent is back to 0, and the Console is emptied. In a real app, the whole page loads again, and all your app’s state is gone.

The fix: take e in the handler and call e.preventDefault() first.

import { useState } from 'react'

export default function App() {
  const [sent, setSent] = useState(0)

  function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
    e.preventDefault()
    console.log('Sending...')
    setSent(sent + 1)
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>Times sent: {sent}</p>
      <button type="submit">Send</button>
    </form>
  )
}
Sending...
Sending...

Run it and click “Send” two times. Now the count goes up with each click, and the Console keeps its lines.

Using e.target when you meant e.currentTarget

The two are often the same, so it’s easy to mix them up.

  • e.target is the element where the event started. That may be deep inside.
  • e.currentTarget is the element whose handler is running now.

Here one handler sits on a <div>, around two buttons:

import { useState } from 'react'

export default function App() {
  const [info, setInfo] = useState('Click a button.')

  function handleClick(e: React.MouseEvent<HTMLDivElement>) {
    const target = e.target as HTMLElement
    setInfo(`target: ${target.tagName}, currentTarget: ${e.currentTarget.tagName}`)
  }

  return (
    <div onClick={handleClick}>
      <button>Save</button>
      <button>Delete</button>
      <p>{info}</p>
    </div>
  )
}

Click “Save”. The page shows target: BUTTON, currentTarget: DIV. The click started on the button and bubbled up to the div. Now click the text under the buttons. target becomes P, the paragraph.

as HTMLElement tells TypeScript “trust me, this is an HTML element”. In a click handler, TypeScript can’t know which element the event started on. So e.target has a loose type, EventTarget.

On an input’s own onChange, it’s different. The event starts at the input, and the input’s handler runs, so target and currentTarget are the same input. React’s types say so too. That is why e.target.value worked with no as in the typing example.

This causes a bug when a button has something inside it, like an icon:

export default function App() {
  function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
    const target = e.target as HTMLElement
    console.log('target id:', target.dataset.id)
    console.log('currentTarget id:', e.currentTarget.dataset.id)
  }

  return (
    <button data-id="save" onClick={handleClick}>
      <span>★</span> Save
    </button>
  )
}

data-id="save" puts a small piece of data on the button. Code reads it back as dataset.id.

Click the star. We checked this in React 19.3: target id was undefined, and currentTarget id was save. The click started on the <span>, and the <span> has no data-id. Click the word “Save” instead, and both are save.

The fix: when you want the element that has the handler, use e.currentTarget.

Surprised by stopPropagation()

A box counts every click inside it. Then someone adds e.stopPropagation() to one button:

import { useState } from 'react'

export default function App() {
  const [clicks, setClicks] = useState(0)

  return (
    <div onClick={() => setClicks(clicks + 1)}>
      <p>Clicks in this box: {clicks}</p>
      <button onClick={(e) => e.stopPropagation()}>Like</button>
      <button>Share</button>
    </div>
  )
}

Click “Like”, then “Share”. The count is 1, not 2. The “Like” click never reached the box.

Use stopPropagation() only when a parent truly must not see the event. If a parent must see every click, even stopped ones, give it onClickCapture instead of onClick. Its handler then runs first, before the button’s.

Reading state right after setting it

Inside a handler, count still has its old value right after setCount(count + 1). Part 4 shows this and explains why.

Practice

Press Edit on any example above and try these.

  1. In “Try this first”, add a second button, “Take one”, that makes the count one smaller.
  2. In the bubbling example, call e.stopPropagation() in the div’s handler, not the button’s. Click the button. Which lines does the Console show?
  3. In the onChange example, add a line that shows how many letters are in the box.
  4. In the form example, don’t save an empty name. If the box is empty, clicking “Save” should do nothing.
Answers
  1. Add <button onClick={() => setCount(count - 1)}>Take one</button>. Each click on it makes the count one smaller.
  2. button onClick, then div onClick. The event still starts at the button and reaches the div. The div stops it, so the section’s handler never runs.
  3. For example: <p>Letters: {name.length}</p>.
  4. Check the name after e.preventDefault(), and stop if it’s empty. return ends the function early.
import { useState } from 'react'

export default function App() {
  const [name, setName] = useState('')
  const [message, setMessage] = useState('')

  function handleSubmit(e: React.SubmitEvent<HTMLFormElement>) {
    e.preventDefault()
    if (name === '') return
    setMessage(`Saved: ${name}`)
    setName('')
  }

  return (
    <form onSubmit={handleSubmit}>
      <input aria-label="Name" value={name} onChange={(e) => setName(e.target.value)} />
      <button type="submit">Save</button>
      <p>{message}</p>
    </form>
  )
}

Keep e.preventDefault() before the check. If it came after the if line, clicking “Save” with an empty box would load the page again. In the playground, the example would start over and the Console would be emptied.

Interview questions

Try to answer each one out loud before you open the answer.

Why do you pass an event handler to onClick, and not call it?

Compare onClick={handleClick} with onClick={handleClick()}. The first passes the function. React keeps it and calls it on each click. The second calls the function while the component renders, and passes whatever it returns, usually undefined. So it runs on every render and never on a click. If it sets state, each render asks for another render, and React throws “Too many re-renders”.

A strong answer adds how to pass a value: wrap the call in an arrow function, onClick={() => handleDelete(id)}. That makes a new function, which React calls later.

What is a synthetic event in React?

It is the event object React passes to your handlers. React makes it, and it wraps the browser’s own event. It has the usual parts: type, target, currentTarget, preventDefault() and stopPropagation(). React’s docs say it follows the same standard as browser events. It also fixes some differences between browsers. The browser’s own event is in e.nativeEvent.

Where does React put its event listeners?

At the root container, the element you passed to createRoot. React doesn’t add a listener to each button. It listens high up, at the root, and works out which handlers to call. This is called event delegation. Before React 17, it listened on document for most events. React’s blog gives the reason. The move makes it safer to put a tree run by one React version inside another one. It also makes it easier to add React to apps built with other tools.

A strong answer adds three details. First, a few events are put on the element itself, not the root. React’s own code lists them, such as load, invalid, toggle and the media events. We checked: rendering an <img onLoad> added load and error listeners to the image. Second, React 19.3 also added one selectionchange listener to document. Third, React walks up its own component tree, not the HTML tree. So a click inside a portal still reaches the onClick of its React parent. A portal shows a component somewhere else in the HTML. In our test, the button in the portal ran its own onClick, then its React parent’s.

What is the difference between stopping the default action and stopping propagation?

They are not related. preventDefault() stops the browser’s default action. For a form, that is sending it and loading the page again. For a link, it is going to the link’s page. stopPropagation() stops the event from bubbling up to the handlers on parent elements. A handler can call one, both or neither.

What is the difference between an event’s target and its current target?

e.target is the element where the event started. e.currentTarget is the element whose handler is running now. They differ when the event bubbled up from a child. Say a button has a <span> inside it. A click on the span gives target = the span and currentTarget = the button.

A strong answer says to use currentTarget when you mean “the element I put the handler on”. It also knows the types. In a click handler, currentTarget gets the element’s type, but target is only EventTarget. On an input’s own onChange, both are the input, and React’s types give target the input’s type too.

How does a child component tell its parent that something happened?

The parent passes a function down as a prop, named like onPick or onDelete. The child calls it from its own event handler, often with a value: onPick(color). The parent’s function then updates the parent’s state. Data goes down as props, and events come back up as function calls.

Does React’s onChange work like the HTML change event?

No. For a text input, React’s onChange runs each time the text changes, like the browser’s input event. The browser’s own change event waits until the box loses focus.

Sources

  • Responding to Events, react.dev: passing vs calling a handler, arrow functions, handler props and their names, bubbling, stopPropagation, the capture phase, preventDefault, and “All events propagate in React except onScroll”.
  • Common components, react.dev: the React event object, its properties and methods, “the values your React code expects”, and “In React, focus events bubble”.
  • <input>, react.dev: onChange “Fires immediately when the input’s value is changed by the user” and “Behaves like the browser input event”.
  • <form>, react.dev: onSubmit, the default action, e.preventDefault(), and the action prop.
  • Using TypeScript, react.dev: typing DOM events such as React.ChangeEvent<HTMLInputElement>.
  • State as a Snapshot, react.dev: why state doesn’t change inside the running handler.
  • React v17.0, React blog, 2020: events are attached to the root container, not document, and why.
  • react-dom 19.3.0, react-dom-client.development.js: React walks from the clicked element up through its parents, collects the handlers, then calls them in order, setting currentTarget for each.
  • @types/react 19.3.0, index.d.ts: the event types, ChangeEvent‘s target typed as the input, and FormEvent marked deprecated (“FormEvent doesn’t actually exist”). SubmitEvent is not in 19.2.9 and first appears in 19.2.10 (published 27 January 2026); we compared the two files.
  • react-dom 19.3.0, react-dom-client.development.js: the nonDelegatedEvents list of events React puts on the element itself.
  • createPortal, react.dev: “Events from portals propagate according to the React tree rather than the DOM tree”.
  • MDN: the change event (fires when a text box loses focus), the input event, and currentTarget.
  • HTML Standard, implicit submission: pressing Enter in a text box submits the form by clicking its first submit button.
  • What the playground does when a form is submitted without e.preventDefault() (the box loads again, the code runs from the start, the Console is emptied) was checked in Chromium and WebKit when the playground was set up.
  • The call counts, the order of handlers, the listener counts, the error text and the target values above come from running React 19.3.0 for this post.
  • This part follows the Event Handling kata in react-katas.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.