Skip to content

Adding Interactivity

Fresh

Some things on the screen update in response to user input. This section covers how to handle events, update state, and understand React's rendering cycle.

Responding to Events

Add event handlers by passing functions as props:

jsx
export default function Button() {
  function handleClick() {
    alert('You clicked me!');
  }

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

Event Handler Conventions

  • Define handlers inside the component (they have access to props and state)
  • Name them handle + event name: handleClick, handleMouseEnter
  • Pass the function, don't call it: onClick={handleClick} not onClick={handleClick()}

Inline Event Handlers

jsx
<button onClick={() => alert('clicked')}>Click</button>

<button onClick={function handleClick() { alert('clicked'); }}>Click</button>

Event Propagation

Events bubble up through the tree. A click on a child button also fires onClick on the parent div:

jsx
function Toolbar() {
  return (
    <div onClick={() => alert('toolbar clicked')}>
      <button onClick={(e) => {
        e.stopPropagation();
        alert('button clicked');
      }}>
        Click me
      </button>
    </div>
  );
}

Preventing Default Behavior

jsx
function Signup() {
  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      alert('Submitting!');
    }}>
      <input />
      <button>Send</button>
    </form>
  );
}

State: A Component's Memory

Components need to "remember" things: the current input value, the current image, the shopping cart. In React, this kind of component-specific memory is called state.

jsx
import { useState } from 'react';

export default function Gallery() {
  const [index, setIndex] = useState(0);
  const [showMore, setShowMore] = useState(false);

  function handleNextClick() {
    setIndex(index + 1);
  }

  function handleMoreClick() {
    setShowMore(!showMore);
  }

  return (
    <div>
      <button onClick={handleNextClick}>Next</button>
      <h2>Sculpture {index + 1}</h2>
      <button onClick={handleMoreClick}>
        {showMore ? 'Hide' : 'Show'} details
      </button>
      {showMore && <p>Details here...</p>}
    </div>
  );
}

How useState Works

  1. useState(initialValue) returns [currentValue, setterFunction]
  2. React preserves state between re-renders
  3. Each render has its own state values (a "snapshot")
  4. Setting state triggers a re-render

State is Isolated and Private

State is local to a component instance. If you render the same component twice, each copy gets its own state. Changing state in one does not affect the other.

Render and Commit

React renders your UI in three steps:

flowchart LR
    A[1. Trigger render] --> B[2. Render component]
    B --> C[3. Commit to DOM]
  1. Triggering a render: initial render (createRoot().render()) or state update (setState)
  2. Rendering the component: React calls your component function. For initial render, React calls the root component. For updates, React calls the component whose state changed.
  3. Committing to the DOM: React applies the minimal necessary changes to the DOM.

After committing, the browser repaints the screen.

React Only Changes What's Different

React only modifies the DOM nodes if there's a difference between renders. It doesn't touch DOM nodes that haven't changed.

State as a Snapshot

Setting state does not change the variable in your existing code. It requests a re-render with a new value:

jsx
function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <button onClick={() => {
      setNumber(number + 1);
      setNumber(number + 1);
      setNumber(number + 1);
      // This only increments by 1! All three calls see number = 0
    }}>+3</button>
  );
}

Each render's state is fixed. The number variable is always the value from that particular render.

Queueing a Series of State Updates

To update state multiple times before the next render, use an updater function:

jsx
function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <button onClick={() => {
      setNumber(n => n + 1); // queue: add 1
      setNumber(n => n + 1); // queue: add 1
      setNumber(n => n + 1); // queue: add 1
      // This increments by 3!
    }}>+3</button>
  );
}
Queued updatenReturns
n => n + 101
n => n + 112
n => n + 123

Updating Objects in State

State can hold any JavaScript value, including objects. But don't mutate objects that you hold in state. Instead, create a new object:

jsx
const [person, setPerson] = useState({
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'barbara@example.com'
});

// WRONG: mutating state directly
person.firstName = 'Marie';

// RIGHT: create a new object
setPerson({
  ...person,
  firstName: 'Marie'
});

Updating Nested Objects

jsx
const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
  }
});

setPerson({
  ...person,
  artwork: {
    ...person.artwork,
    city: 'New Delhi'
  }
});

Updating Arrays in State

Arrays are mutable in JavaScript, but treat them as immutable when stored in state:

OperationAvoid (mutates)Prefer (returns new array)
Addingpush, unshiftconcat, [...arr] spread
Removingsplice, pop, shiftfilter, slice
Replacingsplice, arr[i] = xmap
Sortingsort, reverseCopy first, then sort
jsx
const [artists, setArtists] = useState([]);

// Adding
setArtists([...artists, { id: nextId++, name: name }]);

// Removing
setArtists(artists.filter(a => a.id !== artist.id));

// Replacing
setArtists(artists.map(a => {
  if (a.id === artist.id) {
    return { ...a, name: newName };
  } else {
    return a;
  }
}));

// Inserting at a position
const insertAt = 1;
const nextArtists = [
  ...artists.slice(0, insertAt),
  { id: nextId++, name: name },
  ...artists.slice(insertAt)
];
setArtists(nextArtists);

Use Immer for Complex Updates

The Immer library lets you write mutative-looking code that produces immutable updates:

jsx
import { useImmer } from 'use-immer';
const [person, updatePerson] = useImmer(initialPerson);
updatePerson(draft => { draft.artwork.city = 'Lagos'; });