React Event Handling

Feb 17, 2022#react#javascript

When it comes to events in React, only DOM elements are allowed to have event handlers which make it possible for users to interact with your React app. If you’re familiar with how events work in standard HTML and JavaScript, it should be easy for you to learn how to handle events in React.

Handling events in React is simple but you have to careful about this context when using class methods as event handlers.

Function binding in JavaScript

The context this inside functions can be bound to different objects depending on where the function is being called:

  • When a function is called in the global scope, this is by default bound to the global object (window in browser).
  • When a function is called with a context object, this will be bound to this object.
  • When explicitly bind this at the call site (call(), apply(),bind()), this will be bound to that object.
  • Arrow function binds this to the context of the surrounding code where it’s defined.
  • When a function is called with new keyword, it will create a new object and this will be bound to this object (also call it as constructor function).

Classes in JavaScript are just syntactic sugar atop functions that have a prototype property and that implicitly return an object. When new is called on the function, it calls the function and attaches everything in the function’s prototype to the returned object.

Handling events in class components

When you define a component using a class, a common pattern is for an event handler to be a method on the class. In JavaScript, class methods are not bound by default. Once a method is passed somewhere separately from the object – this is lost.

class Foo extends React.Component {
  constructor(props) {
    super(props)
  }

  handleClick(event) {
    /* "this" will be undefined when event happens */
  }

  render() {
    return <button onClick={this.handleClick}>Click</button>
  }
}

If you forget to bind this.handleClick, the this value falls back to default binding and is set to undefined when the event occurs, as class declarations and prototype methods run in strict mode.

(1) When we bind the this of the event handler to the component instance in the constructor, we can pass it as a callback without worrying about it losing its context.

class Foo extends Component {
  constructor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    /* ... */
  }
  render() {
    return <button onClick={this.handleClick}>Click Me</button>
  }
}

(2) You can use arrow functions (this is bound lexically) with public class fields syntax:

class Foo extends Component {
  handleClick = () => {
    /* ... */
  }
  render() {
    return <button onClick={this.handleClick}>{'Click me'}</button>
  }
}

(3) Or just inline arrow functions (this is bound lexically):

class Foo extends Component {
  handleClick() {
    /* ... */
  }
  render() {
    return <button onClick={() => this.handleClick()}>Click Me</button>
  }
}

The problem with (3) is that a different function is created each time the component renders might trigger rerendering on lower components if passed as props. It is preferred to go with (1) or (2) approach for better performance.

Handling events in function components

You can declare a const function either inside the body of the functional component, or outside.

const Foo = () => {
  const handleClick = (e) => {
    /* ... */
  }
  return <button onClick={handleClick}>Click Me</button>
}

However, it’s sometimes suggested to avoid declaring functions inside a render function to avoid unnecessary re-renders of pure components. React provides the useCallback hook, for memoizing callbacks.

const Foo = () => {
  const handleClick = useCallback(
    (e) => {
      /* ... */
    },
    [
      /* deps */
    ]
  )

  return <Bar onClick={handleClick}>Click</Bar>
}

Difference from HTML event handling

React events are named using camelCase, rather than lowercase like in HTML.

<!--HTML-->
<button onclick="activateLasers()"></button>
// React
<button onClick={activateLasers}>

In HTML, you can return false to prevent default behavior. Whereas in React you must call preventDefault() explicitly.

<!--HTML-->
<a href="#" onclick='console.log("The link was clicked."); return false;' />
// React
function handleClick(event) {
  event.preventDefault()
  /* ... */
}

Passing parameters to event handlers

The event argument, an instance of SyntheticEvent, automatically passed into event handler whenever the event is emitted.

React uses SyntheticEvent as a cross-browser wrapper around the browser’s native event. It has the same interface as the browser’s native event, including stopPropagation() and preventDefault(), except the events work identically across all browsers.

If you find that you need the underlying browser event for some reason, simply use the nativeEvent attribute to get it. Every SyntheticEvent object has the following attributes:

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type

It achieves high performance by automatically using event delegation. In actuality, React doesn’t attach event handlers to the nodes themselves. Instead, a single event listener is attached to the root of the document. When an event is fired, React maps it to the appropriate component element.

There’s a lot of different kinds of events. React events emulated the W3C suite of events, which includes Keyboard, Mouse, Touch, Clipboard, Form events.

(1) With an arrow function, we have to pass event argument explicitly:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>

(2) But with bind any further arguments are automatically forwarded.

<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

(3) You can also use class public fields syntax with curried function.

class Foo extends React.Component {
  handleClick = (id) => (e) => {
    /* ... */
  }

  render() {
    return <button onClick={this.handleClick(id)} />
  }
}

Typing with TypeScript

TypeScript is smart enough to infer types related to DOM events and event handlers because declared in its type:

// Declares there is a global variable called 'window'
declare var window: Window & typeof globalThis
// Which is declared as (simplified):
interface Window extends GlobalEventHandlers {
  // ...
}
// Which defines a lot of known handler events
interface GlobalEventHandlers {
  onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null
  // ...
}

If performance is not an issue (and it usually isn’t!), inlining handlers is easiest as you can just use type inference and contextual typing:

const Foo = () => (
  <button
    onClick={(event) => {
      /* event will be correctly typed automatically! */
    }}
  />
)

But if you need to define your event handler separately, the @type/react definitions come with a wealth of typing. Type what you are looking for and usually the autocomplete will help you out.

class Foo extends React.Component<Props, State> {
  onChange = (e: React.FormEvent<HTMLInputElement>): void => {
    this.setState({text: e.currentTarget.value})
  }
}

Instead of typing the arguments and return values with React.FormEvent<> and void, you may alternatively apply types to the event handler itself.

class Foo extends React.Component<Props, State> {
  onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    this.setState({text: e.currentTarget.value})
  }
}