@onDocument
Listens to an event on document for as long as the component is on the page. Everything true of
@onWindow is true here — the same lifetime, the same refusals,
the same options argument — and only the target differs.
The situation it is for
A dialog that closes on Escape. The keystroke does not happen inside the dialog's markup — it
happens wherever the reader's focus is, which may be nowhere in particular — so no onkeydown
attribute can catch it:
class Dialog extends Component<{ title: string; onClose: () => void }> {
@onDocument("keydown")
escape(e: KeyboardEvent) {
if (e.key === "Escape") this.props.onClose();
}
render() {
return (
<div role="dialog" aria-label={this.props.title}>
<h2>{this.props.title}</h2>
<button type="button" onclick={this.props.onClose}>Close</button>
</div>
);
}
}
The listener exists for exactly as long as the dialog does. Open three dialogs and there are three;
close them and there are none — which is the part that is easy to get wrong by hand, because the
removeEventListener has to be handed the very same function.
Which of the two to reach for
document is where events that bubble end up, so this is the one for a keystroke or a click
anywhere on the page — a shortcut, a menu that closes when you click outside it.
window is where events that are about the viewport or the page itself are dispatched:
resize, scroll, hashchange, beforeunload. Those do not bubble to document at all, so the
choice is not a preference.
What it refuses
The same three as @onWindow: anything but a method, an empty or non-string event name, and — by
the types — "onkeydown" or "KeyDown", each refused with the sentence that says what to write.
What it costs
One listener per instance, attached at mount. A shortcut belongs on the component that owns the action, not on every row that might be affected by it.
Browser-only, like every listener — because this is built on the effect primitive, and effects never run during a server render.