React Basics #6: Event Handling

7 min read

Last time you learned about state and useState, and along the way you naturally used the onClick event handler. It worked, but there are a few things worth knowing about how React handles events. This time we’ll dig into event handling.

How to handle events in React #

In React, you handle an event by passing a handler function as a JSX attribute. The only difference from HTML’s onclick is that you use camelCase onClick.

src/App.jsx
function App() {
  function handleClick() {
    alert('The button was clicked!');
  }

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

export default App;

There’s one common mistake here. Don’t call the function (handleClick()) — pass the function itself (handleClick).

wrong example
<button onClick={handleClick()}>Click</button>

If you write it this way, handleClick() runs the moment the component renders, and the alert pops up. On top of that, what gets registered as onClick is handleClick’s return value (undefined), so an actual click does nothing.

correct example
<button onClick={handleClick}>Click</button>

You pass only the function reference, and React calls it at click time. Remember this distinction.

Inline handlers #

For simple handlers, you can write an arrow function directly inside JSX.

src/Counter.jsx
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

() => setCount(count + 1) is an anonymous function called at click time. For one-liner handlers, inlining like this is convenient; for longer logic, it’s more readable to extract a separate function. There’s no fixed rule — it depends on your team’s or your own preference.

Passing arguments to a function #

When you need to pass an argument to a handler, wrap it in an inline arrow function.

src/App.jsx
function App() {
  function handleClick(name) {
    alert(`Hello, ${name}!`);
  }

  return (
    <>
      <button onClick={() => handleClick('Alice')}>Greet Alice</button>
      <button onClick={() => handleClick('Bob')}>Greet Bob</button>
    </>
  );
}

Don’t write it like this:

wrong example
<button onClick={handleClick('Alice')}>...</button>

As you saw above, this gets called immediately on render. To pass an argument, you must wrap it in an arrow function so it means “call this when clicked.”

The event object #

An event handler receives an event object as its first parameter. This object holds information about which element fired which event.

src/InputDemo.jsx
import { useState } from 'react';

function InputDemo() {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <div>
      <input type="text" value={text} onChange={handleChange} />
      <p>Input value: {text}</p>
    </div>
  );
}

export default InputDemo;

The parameter is usually named e or event. e.target is the DOM element that fired the event, and e.target.value gives you the input’s value.

Note
React’s event object is technically not the browser’s native event but a SyntheticEvent. It’s an object React wraps around the native event so it behaves consistently across all browsers. The API is almost identical to the native event, so you don’t have to think about it day-to-day. You can use familiar properties and methods like e.preventDefault(), e.target, and e.key as is.

Common events #

The most frequently used event handlers:

  • onClick — clicking
  • onChange — when the value of an input element (input, textarea, select) changes
  • onSubmit — when a form is submitted
  • onKeyDown / onKeyUp — when a keyboard key is pressed or released
  • onMouseEnter / onMouseLeave — when the mouse enters or leaves an element
  • onFocus / onBlur — focus enters/leaves

Each event puts the relevant information on the event object. For onChange, look at e.target.value; for onKeyDown, look at e.key (the name of the pressed key).

src/SearchBox.jsx
function SearchBox() {
  function handleKeyDown(e) {
    if (e.key === 'Enter') {
      alert('Enter key was pressed');
    }
  }

  return <input type="text" onKeyDown={handleKeyDown} />;
}

Preventing default behavior #

Browsers have default behaviors for some events. Submitting a form refreshes the page; clicking a link navigates. To prevent the default behavior, call preventDefault() on the event object.

src/LoginForm.jsx
import { useState } from 'react';

function LoginForm() {
  const [email, setEmail] = useState('');

  function handleSubmit(e) {
    e.preventDefault();  // prevent the page refresh caused by form submission
    console.log('submitted email:', email);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />
      <button type="submit">Log in</button>
    </form>
  );
}

export default LoginForm;

A form fires onSubmit automatically when you click the submit button or press Enter inside an input. Without e.preventDefault(), the browser would refresh the page, defeating any handling logic you wrote.

Passing event handlers as props #

An event handler is just a function, so you can pass it as a prop to a child component. This pattern is very common when a parent needs to handle an event raised by its child.

src/Button.jsx
function Button({ label, onClick }) {
  return (
    <button onClick={onClick} style={{ padding: '8px 16px' }}>
      {label}
    </button>
  );
}

export default Button;
src/App.jsx
import Button from './Button';

function App() {
  function handleSave() {
    alert('Saved');
  }

  function handleCancel() {
    alert('Cancelled');
  }

  return (
    <>
      <Button label="Save" onClick={handleSave} />
      <Button label="Cancel" onClick={handleCancel} />
    </>
  );
}

The parent passes down what to do (the handler), and the child reports when that thing happens (a click). By convention, handler prop names start with on (onClick, onSave, onItemSelect, …).

Updating state inside a handler #

You saw this in the previous article — updating state inside an event handler is the most common pattern.

src/Toggle.jsx
import { useState } from 'react';

function Toggle() {
  const [isOn, setIsOn] = useState(false);

  function handleToggle() {
    setIsOn(prev => !prev);
  }

  return (
    <div>
      <p>Current: {isOn ? 'ON' : 'OFF'}</p>
      <button onClick={handleToggle}>Toggle</button>
    </div>
  );
}

export default Toggle;

An event happens → the handler runs → state updates → the screen re-renders. This flow is the most fundamental pattern in a React app.

Try it yourself #

Let’s build a simple input form. The user enters a name and a message, and pressing “Add” displays the message below (we’ll extend this in #7 and #8).

Create src/MessageForm.jsx.

src/MessageForm.jsx
import { useState } from 'react';

function MessageForm() {
  const [name, setName] = useState('');
  const [message, setMessage] = useState('');
  const [lastSubmitted, setLastSubmitted] = useState(null);

  function handleSubmit(e) {
    e.preventDefault();
    if (!name || !message) return;
    setLastSubmitted({ name, message });
    setName('');
    setMessage('');
  }

  return (
    <div style={{ padding: '16px', border: '1px solid #ccc', borderRadius: '8px' }}>
      <form onSubmit={handleSubmit}>
        <div>
          <input
            type="text"
            placeholder="Name"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div style={{ marginTop: '8px' }}>
          <input
            type="text"
            placeholder="Message"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
          />
        </div>
        <button type="submit" style={{ marginTop: '8px' }}>Add</button>
      </form>
      {lastSubmitted && (
        <p style={{ marginTop: '12px' }}>
          Last submitted: <strong>{lastSubmitted.name}</strong>  {lastSubmitted.message}
        </p>
      )}
    </div>
  );
}

export default MessageForm;

Wire it up in src/App.jsx.

src/App.jsx
import MessageForm from './MessageForm';

function App() {
  return (
    <>
      <h1>Message form</h1>
      <MessageForm />
    </>
  );
}

export default App;

Type a name and a message and press Enter or the “Add” button. The most recently submitted value appears below, and the input fields are cleared. Try removing e.preventDefault() once and see how the form refreshes the page and your input vanishes.

Tip
The lastSubmitted && (...) part is something you haven’t seen before — it’s conditional rendering. The pattern is: show this when the value is truthy, hide it otherwise. We’ll cover it in detail in #7.

Wrapping up #

In this article we looked at React’s event handling. The essentials:

  • Register handlers via camelCase attributes like onClick
  • Pass the function — don’t call it ({handleClick}, not {handleClick()})
  • Wrap in an arrow function to pass arguments
  • Handlers receive the event object (e) as their first parameter
  • Use e.preventDefault() to block the browser’s default behavior
  • Handlers can be passed down to children as props (names that start with on)

Also, the {lastSubmitted && ...} pattern you saw briefly in the MessageForm example leads directly into the next topic. In the next article, “React Basics #7: Conditional Rendering,” we’ll round up the various patterns for showing, hiding, or swapping parts of the screen based on state.

X