React Button Click Does Nothing - Or Fires Before You Click
5 min read
“Two bugs, same family. Either the click handler runs the moment the page loads and never again - or it runs on every click and does the exact same thing every time. Both come down to what you actually wrote inside onClick.”
You wire up a button. onClick={doTheThing}. Looks right. Reads right.
And then one of two things happens. Either doTheThing runs immediately,
before anyone has clicked anything - or it runs on every click, but it does
the same thing every time, like the click never mattered.
Both bugs live in the same spot: the difference between passing a function
and calling one.
Look at onClick={handleSubmit()}. Those parentheses mean "call this
function right now, during render, and use whatever it returns as the
onClick value."
So that is what happens. While React is rendering this component -
before the button even exists on screen - handleSubmit() runs.
setSubmitted(true) fires. React schedules a re-render. The "Thanks for
your rating" message appears immediately, and the button is gone before
you ever saw it.
Symptom
The "after" state shows up the instant the page loads. The button either
never appears, or appears and does nothing - because by the time you click
it, the thing it was supposed to trigger already happened.
onClick={handleSubmit} passes the function itself - a reference, not a
call. React holds onto that reference and invokes it later, only when the
button is actually clicked.
This is the single most common typo in React event handlers, and it is easy
to miss because onClick={handleSubmit()} and onClick={handleSubmit}
look almost identical. One runs on render. One runs on click. The parens
are the entire difference.
The Sensei's Hint
A useful gut check: if your JSX has onClick={something()}, read it out
loud as "call something, right now, and use the result." If that sentence
doesn't make sense for a click handler, the parentheses don't belong there.
The second bug looks completely different but comes from the same place -
writing what should happen instead of what the click should cause.
export default function SecretMessage() { const [visible, setVisible] = useState(false); return ( <div> {visible && <p>The secret is: React re-renders on state change.</p>} <button onClick={() => setVisible(false)}> {visible ? "Hide Secret" : "Show Secret"} </button> </div> );}
This button's onClick is correctly written as an arrow function - no
calling-too-early bug here. But look at what it does: setVisible(false).
Every click, no exceptions, sets visible to false.
Since visible starts as false, the first click sets it to false again.
Object.is(false, false) is true. React sees no change and skips the
re-render. The secret never appears, the button label never changes, and
every future click repeats the exact same no-op.
The Sensei's Hint
This one is sneaky because the handler does run - you can put a
console.log in there and watch it fire on every click. The bug isn't
that the click is ignored. It's that the click always produces the same
instruction.
(v) => !v is a functional updater - it receives whatever visible
currently is and returns its opposite. The button no longer says "make
visible false." It says "flip whatever visible is right now." That
instruction produces a different result every time, which is the entire
point of a toggle.
Both of these bugs come from writing an instruction that doesn't actually
depend on the click.
onClick={handleSubmit()} doesn't wait for a click - it runs immediately.
onClick={() => setVisible(false)} doesn't depend on the current
state - it always says the same thing.
A correct click handler is either a bare function reference (onClick={fn})
or an arrow function whose body contains the logic, and if that logic needs
the current state, it should read that state - directly, or through a
functional updater like (v) => !v or (c) => c + 1.
Constraints
If you can describe what your onClick does without the word "click" in
the sentence, something is wrong. "It sets submitted to true" (happens on
render). "It sets visible to false" (happens the same way every time).
Neither sentence needs a click to be true - and neither should pass code
review written this way.
Both katas are one-line fixes once you see them, which is exactly why they
are worth doing slowly. "Handler Runs on Load, Not on Click" is the
parentheses bug in isolation - watch the thank-you message appear before you
touch anything. "Toggle That Won't Toggle" is the hardcoded-value bug -
click the button, read the label, and ask yourself what setVisible(false)
is actually telling React to do, every single time.