Skip to main content

Handling Events

Events and State Together

Written by Published

When two components need the same value, it belongs to their closest shared parent. Moving it there is called lifting state up, and it is the answer to most "how do these talk to each other" questions.

A handler that updates state

The pattern is always the same: an event fires, the handler computes the next value, the setter stores it, React re-renders. Everything else in this chapter is a variation on those four steps.

Toggle, add, reset

jsx

function Panel() {
  const [open, setOpen] = React.useState(false)
  const [notes, setNotes] = React.useState([])

  return (
    <div>
      <button onClick={() => setOpen((current) => !current)}>
        {open ? "Hide" : "Show"} notes
      </button>{" "}
      <button onClick={() => setNotes((current) => [...current, `Note ${current.length + 1}`])}>
        Add note
      </button>{" "}
      <button onClick={() => setNotes([])}>Clear</button>

      {open && (
        <ul>
          {notes.map((note) => <li key={note}>{note}</li>)}
          {notes.length === 0 && <li style={{ color: "#64748b" }}>No notes yet</li>}
        </ul>
      )}
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Panel />)

Lifting state up

Two sibling components cannot see each other's state. If both need the same value, it moves up to the nearest parent that contains them both, and comes back down as props.

One value, two children

jsx

function Display({ value }) {
  return <p style={{ fontSize: 20 }}>Current: <strong>{value}</strong></p>
}

function Controls({ onChange }) {
  return (
    <div style={{ display: "flex", gap: 6 }}>
      <button onClick={() => onChange((n) => n - 1)}></button>
      <button onClick={() => onChange((n) => n + 1)}>+</button>
      <button onClick={() => onChange(0)}>Reset</button>
    </div>
  )
}

function Parent() {
  // The state lives here because both children need it.
  const [value, setValue] = React.useState(0)

  return (
    <div>
      <Display value={value} />
      <Controls onChange={setValue} />
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<Parent />)

Display and Controls know nothing about each other. One receives a value, the other receives a function, and the parent is the only place the state actually exists.

How far up?

  1. Find every component that reads the value.
  2. Find their closest common ancestor.
  3. Put the state there - no higher.

Pushing state further up than necessary makes the top of the app own everything and re-render constantly. It is a real cost, and the reason "just put it all in one place" stops working as an app grows.

Passing the setter versus wrapping it

onChange={setValue} hands the child the raw setter, which is fine when the child should be able to set any value. Wrapping it - onChange={(n) => setValue(clamp(n))} - keeps the rule in the parent, where the state lives.

Prefer the wrapper when there is a constraint. The parent owns the value, so it should own what counts as a valid one.