Get eBook BundleVolume I index
State and Events From Scratch

Click Handlers and Event Props

Ishtmeet Singh @ishtms/July 20, 2026/13 min read
#react#events#handlers#onclick#interaction

A component runs while React is figuring out what UI should be there. An event handler runs later, after that UI is already on the page and somebody actually does something with it.

That difference sounds small, but you'll use it all the time while writing React.

Take a button:

jsx
function ContinueButton() {
  function handleClick() {
    console.log("Continue reading");
  }

  return <button onClick={handleClick}>Continue</button>;
}

When ContinueButton renders, handleClick does not run. React just gets the function through the onClick prop. React DOM gets the button into the DOM, and later, if the user clicks it, React calls handleClick.

So the order is roughly:

text
component renders
  -> button gets the handler
  -> React DOM commits the button
  -> user clicks it
  -> handler runs

This is why user-caused work usually starts inside handlers. If somebody clicks Save, save from that event. If they click Delete, start deletion from there. State updates caused by that click also normally begin there.

Pass the function, don't call it

This mistake is very easy to make when you're new to React:

jsx
<button onClick={handleClick}>Continue</button>

versus:

jsx
<button onClick={handleClick()}>Continue</button>

They look almost the same, but JavaScript sees two completely different things.

In the first version, you're giving React the function itself. React can keep that function and call it later when the click happens.

In the second one, those () mean "call this function now". And since this JSX is being evaluated during render, handleClick() runs during render.

Then whatever it returns gets assigned to onClick.

Most handlers don't return another function, so usually you're effectively doing this:

jsx
<button onClick={undefined}>Continue</button>

You'll notice the bug pretty quickly. Your log, navigation, state update, whatever, happens as soon as the component appears. Then you click the button and nothing happens.

So for a normal named handler:

jsx
<button onClick={handleClick}>Continue</button>

No parentheses.

An inline arrow works too because the arrow itself is the function you're passing:

jsx
<button onClick={() => console.log("Continue")}>
  Continue
</button>

React calls that arrow after the click, and only then does console.log() run.

What if I need to pass some value?

This is usually where people add the parentheses again.

Say every lesson has an ID and you need to open the one that was clicked:

jsx
function LessonItem({ lesson, onOpen }) {
  return (
    <button onClick={() => onOpen(lesson.id)}>
      {lesson.title}
    </button>
  );
}

The arrow captures lesson.id from this render. Nothing gets opened while rendering. Later the user clicks, React calls the arrow, and the arrow calls onOpen(lesson.id).

If you write this instead:

jsx
<button onClick={onOpen(lesson.id)}>
  {lesson.title}
</button>

then onOpen() runs immediately while rendering again. Same problem, just with an argument now.

You can also move it into a local function if the JSX starts getting noisy:

jsx
function handleOpen() {
  onOpen(lesson.id);
}

return <button onClick={handleOpen}>{lesson.title}</button>;

Both versions are fine. Use whichever one reads better in that component.

And don't start worrying that every inline arrow is somehow a performance disaster. You'll find plenty of old advice around this. Get the code working and readable first. If you later profile the application and actually find callback identity causing some problem, then deal with that problem.

onClick and your own callback props are different

When you're working with real DOM elements in React, event prop names are already decided for you.

You have things such as onClick, onChange, onSubmit, onKeyDown, etc.

But once you're passing a callback into your own component, you decide what that prop is called.

For example:

jsx
<LessonItem
  lesson={lesson}
  onOpenLesson={handleOpenLesson}
/>

Then inside LessonItem:

jsx
function LessonItem({ lesson, onOpenLesson }) {
  return (
    <button onClick={() => onOpenLesson(lesson.id)}>
      {lesson.title}
    </button>
  );
}

onOpenLesson is not some browser event React knows about. We invented that prop name because opening a lesson is the action our component exposes.

The child knows there's a button being clicked. The parent doesn't really need to care which exact DOM interaction caused it. The parent just needs to know, "open this lesson".

I usually use onSomething for callback props and handleSomething for the local function that handles them.

jsx
function handleOpenLesson(lessonId) {
  console.log("open", lessonId);
}

So you might have an onOpenLesson prop coming into a component, and a handleOpenLesson function inside another component implementing that behavior.

It's only a naming convention, React isn't enforcing this. But once a codebase gets bigger it becomes quite easy to read.

Also, giving a custom prop a name starting with on doesn't magically register any browser event.

This:

jsx
<LessonItem onOpenLesson={handleOpenLesson} />

only passes a function as a prop. LessonItem still has to use that function somewhere.

React gives your handler an event object

React calls event handlers with an event object.

jsx
function handleClick(event) {
  console.log(event.type);
}

Click the button and event.type will be "click".

You'll use this object when you need information about what happened or when you need to change some browser event behavior.

For example:

jsx
event.preventDefault();
event.stopPropagation();
event.target;
event.currentTarget;

If for some lower-level reason you really need the original native browser event, React exposes it through:

jsx
event.nativeEvent

Most of the time you won't need that. The normal React event object gives component code what it needs.

You may also come across older React code calling:

jsx
event.persist();

That came from older versions where React event objects were pooled. Modern React doesn't pool these events, so you don't need persist() just to read the event later.

target and currentTarget

These two names look similar enough that you'll probably mix them up once or twice.

Let's put a span inside a button:

jsx
function handleClick(event) {
  console.log(event.target);
  console.log(event.currentTarget);
}

return (
  <button onClick={handleClick}>
    <span>Continue</span>
  </button>
);

Now click directly on the word Continue.

The span can be event.target, because that's the deepest element where the event started.

But event.currentTarget is the button while the button's click handler is running, because that's the element this handler belongs to.

So if your handler means "give me the element I attached this handler to", you usually want currentTarget.

jsx
function handleClick(event) {
  event.currentTarget.focus();
}

This becomes more useful once buttons contain icons, spans, SVGs, and other nested stuff. target can change depending on exactly what inside the button got clicked. currentTarget still refers to the button for that button handler.

Click events can move up through parent elements

Browser events usually propagate through ancestor elements.

For example:

jsx
function LessonCard() {
  function handleCardClick() {
    console.log("card");
  }

  return (
    <article onClick={handleCardClick}>
      <button type="button">Bookmark</button>
    </article>
  );
}

The button has no click handler here.

But click the button and the click can still reach the article, so handleCardClick() runs.

Now give the button its own handler:

jsx
<button type="button" onClick={handleBookmarkClick}>
  Bookmark
</button>

When you click it, the button handler runs and then the event continues upward, so the article's click handler can run too.

Sometimes that's exactly what you want. Sometimes it's really not.

Maybe clicking anywhere on the card opens the lesson, but clicking Bookmark should only bookmark it and should not also open the lesson.

Then you can stop the event from continuing upward:

jsx
function handleBookmarkClick(event) {
  event.stopPropagation();
  saveBookmark();
}

After stopPropagation(), later ancestors don't receive that same event through the normal propagation path.

Don't put stopPropagation() into every handler just because you discovered it exists, though. Parent handlers may intentionally depend on those events. Use it when that nested interaction really should be separate.

There's also a capture phase

Normal onClick handlers generally run as the event moves upward from the target.

React also lets you listen during the capture phase by adding Capture to the event prop name:

jsx
<section onClickCapture={handleCapturedClick}>
  <button onClick={handleButtonClick}>Continue</button>
</section>

The section's capture handler gets a chance before the normal target and bubbling handlers.

You probably won't use capture much in normal UI code. It can be useful for some logging and event coordination cases, but a regular button action doesn't need it.

One event you'll want to remember behaves differently is onScroll. React does not bubble onScroll through host ancestors, so put that handler on the element that's actually scrolling.

preventDefault() is doing a different job

Some browser events already have an action attached to them.

A form submission can send the form and load another document. A link normally follows its href.

Sometimes your React code wants to handle the action instead.

For a form:

jsx
function handleSubmit(event) {
  event.preventDefault();
  console.log("save in client state");
}
jsx
<form onSubmit={handleSubmit}>
  <button type="submit">Save</button>
</form>

preventDefault() tells the browser not to perform the normal default action for that event.

This is different from:

jsx
event.stopPropagation();

stopPropagation() controls whether the event continues through ancestors.

preventDefault() controls the browser's default action.

You can call one without the other.

Also don't cancel normal browser behavior and then manually rebuild the exact same behavior for no reason. If a link should simply take the user to another URL, a real anchor with href already knows how to do that.

jsx
<a href="/lessons/events">Events lesson</a>

You don't need to intercept every click just because you're using React.

Put submit handling on the form

Suppose you have:

jsx
<form onSubmit={handleSubmit}>
  <input name="query" />
  <button type="submit">Search</button>
</form>

This is the normal place for submission logic.

Why not just put onClick on the button?

Because clicking that button isn't the only way a form can submit. A user can submit from the keyboard too, depending on the controls in the form.

If your logic only lives here:

jsx
<button type="submit" onClick={handleSubmit}>
  Search
</button>

you're handling a button click, not the form's submission itself.

Put submit behavior on onSubmit of the form, and let the form tell you when it was submitted.

Buttons inside forms have a default type

This one has caused enough "why is my page submitting??" debugging sessions.

Take this:

jsx
<form>
  <button onClick={handleReset}>Reset filters</button>
</form>

That button has no explicit type.

Inside a form, a button defaults to being a submit button. So clicking Reset filters can also submit the form.

If the button is just some normal action, say it:

jsx
<button type="button" onClick={handleReset}>
  Reset filters
</button>

And for the button that actually submits:

jsx
<button type="submit">
  Search
</button>

I prefer putting the type explicitly on buttons inside forms even when the default happens to be what I want. You can look at the JSX and immediately know what that button is supposed to do.

Use an actual button when you need a button

You can make a div respond to clicks:

jsx
<div onClick={handleContinue}>Continue</div>

React will happily run that click handler.

But the browser still sees a div.

It doesn't automatically get the keyboard behavior, focus behavior, accessibility semantics, and form behavior that a real button already has.

So for an action:

jsx
<button type="button" onClick={handleContinue}>
  Continue
</button>

And for navigation:

jsx
<a href="/learn/state-events-from-scratch">
  Read the chapter
</a>

React event props don't change what HTML elements mean. React DOM creates the element you asked for, then browser behavior for that element still comes from HTML and the browser.

So choose the correct element first, then add whatever React event handling you need.

Event handlers can do side effects

Earlier we said component rendering should stay pure. You don't want random HTTP calls, DOM mutations, clipboard writes, or storage changes happening just because React decided to render a component.

Event handlers are different because they run due to some user interaction.

If the user clicked Copy:

jsx
function handleCopy() {
  navigator.clipboard.writeText(lessonUrl);
}

If they clicked Print:

jsx
function handlePrint() {
  window.print();
}

That's the point where this work was requested.

Same with state updates. A button gets clicked, the handler runs, and the handler can call a state setter.

Async work can begin there too:

jsx
async function handleSave() {
  await saveGoal();
}

Of course, once you're doing async work, you still need to deal with failure, loading state, permissions, cancellation, or whatever that particular operation needs. Putting code inside an event handler doesn't make those problems disappear.

It only gives the work the correct trigger.

Errors inside handlers happen later

Look at this:

jsx
function ContinueButton() {
  function handleClick() {
    missingFunction();
  }

  return <button onClick={handleClick}>Continue</button>;
}

The component can render completely fine.

The error only appears after somebody clicks the button because that's when missingFunction() is called.

This is another reason render errors and event-handler errors shouldn't get mixed together in your head. They happen at different times.

If an operation can reasonably fail, deal with that failure where the operation runs.

For example:

jsx
async function handleSave() {
  try {
    await saveGoal();
  } catch (error) {
    console.error(error);
    // show useful feedback to the user
  }
}

What you probably don't want is this:

jsx
try {
  await saveGoal();
} catch (error) {
  // ignored
}

Now the save failed, the user doesn't know what happened, and you've thrown away the error that could tell you why.

Very helpful.

A handler sees values from the render that created it

Every render creates new function bindings.

Take this:

jsx
function LessonItem({ lesson }) {
  function handleOpen() {
    console.log(lesson.id);
  }

  return <button onClick={handleOpen}>{lesson.title}</button>;
}

handleOpen has access to the lesson value from this particular render.

If the parent later renders LessonItem with another lesson, that render creates another handleOpen function which closes over the new lesson.

State behaves the same way.

A handler sees the state values from the render that created that handler.

This becomes much more interesting once we start doing multiple state updates and async callbacks, because sometimes you'll expect a handler to see a "latest" value and it doesn't. We'll get to that.

For now, just remember that handlers aren't reaching into some permanently changing state variable. Each render creates its own values and its own functions using those values.

Let's build the first real interaction

We'll keep the application action in the parent:

jsx
function App() {
  function handleOpenLesson(lessonId) {
    console.log(`Open ${lessonId}`);
  }

  return (
    <LessonItem
      lesson={{ id: "events", title: "Click Handlers" }}
      onOpenLesson={handleOpenLesson}
    />
  );
}

Then LessonItem handles the browser interaction and passes the useful application value upward:

jsx
function LessonItem({ lesson, onOpenLesson }) {
  return (
    <button onClick={() => onOpenLesson(lesson.id)}>
      {lesson.title}
    </button>
  );
}

Click the button and you'll see:

text
Open events

That's really the full chain.

During render, the button receives a function. React DOM gets the actual button onto the page. The user clicks it later. React calls the handler with the event. Our handler then does whatever work belongs to that click.

text
render passes a function
  -> React DOM commits the element
  -> user interacts with it
  -> React calls the handler
  -> handler performs the action

And once you've confirmed it works, remove the temporary console.log.

Otherwise six months later somebody is opening DevTools and wondering why the application is yelling Open events every time they click a lesson.