Week 5

State with useState

Render and commit lifecycle

Event Handling

Forms

Practice

Assignment

Front end Track

How to handle events

Every time a user clicks a button, types into a field, or submits a form, the browser fires an event — a signal that something happened. Event handling is how your code listens for those signals and responds to them.

In vanilla JavaScript you'd write something like document.getElementById('btn').addEventListener('click', handleClick). React wraps that system in something cleaner and more consistent, but the underlying idea is the same: you tell React which element to listen to, what kind of event to watch for, and what function to run when it happens.

This is the foundation of interactivity. Without event handling, your UI is a static page. With it, things actually do stuff.

Attaching Event Handlers in JSX

In React, you attach event handlers directly in JSX as props. The prop name is the event type in camelCase, and the value is the function you want to run.

function LikeButton() {
  function handleClick() {
    console.log('Liked!');
  }

  return <button onClick={handleClick}>Like this post</button>;
}

A few things worth noting here. The prop is onClick (camelCase), not onclick as you'd see in raw HTML. And the value is {handleClick}: a reference to the function, not a function call. Writing onClick={handleClick()} would call the function immediately when the component renders, which is almost never what you want.

<aside> ⚠️

Warning

onClick={handleClick} ✅ — passes a reference, runs on click

onClick={handleClick()} ❌ — calls the function immediately on render

The same trap exists with inline arrow functions. onClick={() => alert('hi')} ✅ wraps the call in a function. onClick={alert('hi')} ❌ runs alert during render.

</aside>

You can also write the handler inline as an arrow function. This is useful for short, one-liner responses:

<button onClick={() => console.log('Liked!')}>Like this post</button>

For anything more than a single expression, define a named function. It's easier to read, easier to test, and easier to debug.

By convention from the React docs, event handlers are named starting with handle followed by the event: handleClick, handleMouseEnter, handleFormSubmit. You'll see this pattern everywhere in React codebases.

The Three Handlers You'll Use Most

onClick — Responding to Clicks

onClick fires when a user clicks any element: buttons, divs, images, links, you name it. In practice, you'll mostly use it on buttons, since those carry the right semantic meaning for users and screen readers.

function ShoppingCart() {
  function handleAddToCart() {
    console.log('Item added to cart');
    // In a real app, this would update cart state
  }

  return (
    <button onClick={handleAddToCart}>
      Add to cart
    </button>
  );
}

<aside> 💡

Info

Buttons are the right element for actions. Avoid putting onClick on a <div> unless you have a specific reason — it breaks keyboard navigation and screen readers. If it does something when clicked, it should be a <button>. You can always style a button to look like anything else with CSS.

</aside>

onChange — Responding to Input Changes

onChange fires every time the value of an input, textarea, or select changes — meaning on every keystroke, not just when the user finishes typing.

function SearchBar() {
  function handleSearch(e) {
    console.log('Searching for:', e.target.value);
  }

  return (
    <input
      type="text"
      placeholder="Search products..."
      onChange={handleSearch}
    />
  );
}

Notice that handleSearch receives e as a parameter. That's the event object (more on that in a moment).

e.target refers to the DOM element that triggered the event (the input in this case), and e.target.value is the current value of that input.

function SubscribeForm() {
  function handleEmailChange(e) {
    console.log('Email so far:', e.target.value);
  }

  function handleFrequencyChange(e) {
    console.log('Frequency chosen:', e.target.value);
  }

  return (
    <div>
      <input
        type="email"
        placeholder="[email protected]"
        onChange={handleEmailChange}
      />
      <select onChange={handleFrequencyChange}>
        <option value="daily">Daily digest</option>
        <option value="weekly">Weekly roundup</option>
        <option value="never">Unsubscribe</option>
      </select>
    </div>
  );
}

<aside> ⌨️

Hands On

Create a simple component with a text input and an onChange handler. Log e.target.value to the console. Type something and watch the console update character by character. Now try changing onChange to onBlur — what's different?

</aside>

onSubmit — Responding to Form Submission

onSubmit fires when a user submits a form, either by clicking a submit button or pressing Enter in a text field.

function ContactForm() {
  function handleSubmit(e) {
    e.preventDefault(); // Stops the page from reloading — see next section

    console.log('Form submitted!');
    // In a real app, you'd gather the form data and send it somewhere
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" placeholder="Your name" />
      <input type="email" placeholder="Your email" />
      <button type="submit">Send message</button>
    </form>
  );
}

Notice that onSubmit goes on the <form> element, not the button. That means the handler fires regardless of how the form was submitted (button click or Enter key).

The Event Object

Every event handler receives an event object as its first argument. React calls these synthetic events — they're React's own version of the browser's native event objects, normalised for consistency across browsers.

By convention, the parameter is typically named e:

function ProfileCard() {
  function handleClick(e) {
    console.log('Event type:', e.type);         // "click"
    console.log('Target element:', e.target);   // the DOM element clicked
  }

  return <button onClick={handleClick}>View profile</button>;
}

Here are the properties you'll reach for most often:

Property What it gives you
e.target The element that triggered the event
e.target.value The current value of an input element
e.target.checked The checked state of a checkbox
e.type The type of event ("click", "change", etc.)
e.key Which key was pressed (for keyboard events)

Preventing Default Behaviour

Some HTML elements have built-in browser behaviours that fire alongside your event handler. The two most common ones you'll need to override:

e.preventDefault() stops the browser from running its default action.

function LoginForm() {
  function handleSubmit(e) {
    e.preventDefault(); // Without this, the page would reload

    const form = e.target;
    const username = form.username.value;
    const password = form.password.value;

    console.log('Logging in as:', username);
    // In a real app: send credentials to your API
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" type="text" placeholder="Username" />
      <input name="password" type="password" placeholder="Password" />
      <button type="submit">Log in</button>
    </form>
  );
}

<aside> ⚠️

Warning

If you forget e.preventDefault() on a form's onSubmit, your handler will run briefly — but then the page reloads and wipes everything. This is one of the most common React beginner bugs. If your form seems to "do nothing", check for this first.

</aside>

<aside> ⌨️

Hands On

Build a form with a single email input and a submit button. In the onSubmit handler:

  1. Call e.preventDefault()
  2. Log the submitted email address using e.target.email.value (this works when the input has name="email")

Then try removing e.preventDefault() and observe what happens.

</aside>

Event Propagation

Here's something that surprises a lot of developers when they first encounter it. Event handlers don't just catch events on their own element — they also catch events from any child elements nested inside them. The event "bubbles" or "propagates" upward through the tree, from where it started all the way to the top.

Consider this toolbar:

function Toolbar() {
  return (
    <div onClick={() => console.log('Toolbar was clicked')}>
      <button onClick={() => console.log('Play clicked')}>
        Play Movie
      </button>
      <button onClick={() => console.log('Upload clicked')}>
        Upload Image
      </button>
    </div>
  );
}

If you click the "Play Movie" button, you'll see two messages in the console — first 'Play clicked' from the button's own handler, then 'Toolbar was clicked' from the parent <div>. The click event bubbles up from the button to the div.

If you click the toolbar area itself (not a button), only 'Toolbar was clicked' appears.

<aside> 💡

Info

All events propagate in React except onScroll, which only fires on the element it's directly attached to.

</aside>

Stopping Propagation

If you want an event to stay on the element that triggered it and not travel to parent handlers, call e.stopPropagation():

function Button({ onClick, children }) {
  return (
    <button
      onClick={(e) => {
        e.stopPropagation(); // Stop the event from reaching parent elements
        onClick();           // Still call the handler passed in from the parent
      }}
    >
      {children}
    </button>
  );
}

function Toolbar() {
  return (
    <div onClick={() => console.log('Toolbar clicked')}>
      <Button onClick={() => console.log('Playing!')}>
        Play Movie
      </Button>
      <Button onClick={() => console.log('Uploading!')}>
        Upload Image
      </Button>
    </div>
  );
}

Now clicking a button logs only that button's message. The toolbar handler never fires.

<aside> ⚠️

</aside>

Passing Handlers as an Alternative to Propagation

The React docs suggest a pattern worth knowing: rather than relying on events bubbling up silently, pass explicit handler props and call them manually. This gives you complete control and makes the chain of events easy to trace.

function Button({ onClick, children }) {
  return (
    <button
      onClick={(e) => {
        e.stopPropagation();
        onClick(); // Explicitly call the parent's handler
      }}
    >
      {children}
    </button>
  );
}

You can add logic before or after calling onClick(), and you always know exactly what runs and in what order (which makes debugging much easier than following implicit bubbling behaviour).

<aside> ⌨️

</aside>

Passing Handlers as Props

As your component tree grows, you'll often want a parent component to decide what happens when something in a child component is interacted with. React handles this by passing event handlers as props.

// Button doesn't know what happens on click — it just passes it through
function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function MediaToolbar() {
  function handlePlayClick() {
    console.log('Starting playback...');
  }

  return (
    <div>
      <Button onClick={handlePlayClick}>Play Movie</Button>
      <Button onClick={() => console.log('Uploading...')}>Upload Image</Button>
    </div>
  );
}

The Button component is reusable because it owns the look, not the behaviour. The parent decides what clicking it does. This is a pattern you'll use constantly when building component libraries or design systems.

Naming Event Handler Props

For built-in elements like <button> and <input>, React uses the standard browser event names: onClick, onChange, onSubmit. When you're building your own components and exposing event-like props, follow the same convention: start with on, followed by a capital letter.

// ✅ Good — consistent with React's naming convention
function VideoPlayer({ onPlay, onPause }) {
  return (
    <div>
      <button onClick={onPlay}>Play</button>
      <button onClick={onPause}>Pause</button>
    </div>
  );
}

// Usage
<VideoPlayer
  onPlay={() => console.log('Playing')}
  onPause={() => console.log('Paused')}
/>

Naming props after what they represent in your app (onPlay, onDeleteItem, onCheckout) rather than generic actions (onClick1, onClick2) makes your components more readable and easier to compose.

Passing Arguments to Handlers

Sometimes you need to pass extra data to a handler: for example, which item in a list was clicked. You can do this by wrapping the call in an arrow function.

function ProductList() {
  const products = [
    { id: 1, name: 'Wireless headphones' },
    { id: 2, name: 'Mechanical keyboard' },
    { id: 3, name: 'USB-C hub' },
  ];

  function handleAddToCart(productId) {
    console.log('Added product ID:', productId);
  }

  return (
    <ul>
      {products.map((product) => (
        <li key={product.id}>
          {product.name}
          <button onClick={() => handleAddToCart(product.id)}>
            Add to cart
          </button>
        </li>
      ))}
    </ul>
  );
}

The arrow function () => handleAddToCart(product.id) creates a new function that closes over the current product.id — a concept called closure, which you covered in the core course. React calls that new function when the button is clicked, which in turn calls handleAddToCart with the right ID.

<aside> ⌨️

</aside>

Event Handlers and Side Effects

One thing worth calling out explicitly: event handlers are the right place for side effects in React. A side effect is anything that reaches outside the current function: logging, network requests, updating the DOM, changing data.