There are 2 ways to set event listeners in react.
Automatically bound listeners
These types of listeners are the ones set by passing a function to an on*
method inside of render()
. Eg.
import React from "react";
interface IProps {}
class Modal extends React.Component<IProps> {
public render() {
<div className="modal" onKeyDown={this.handleEscape}>
{this.state.wasEscapePressed ? "Escape Pressed!" : "Hello world."}
</div>
}
private handleEscape = (event: React.KeyboardEvent) => {
if(event.key === "escape") {
this.closeModal();
}
}
private closeModal() {
// Do something to close the modal.
}
}
In this example handleEscape()
is automatically bound to the element it is assigned to. If the component is removed from the page the underlying event listener will be removed automatically because it is managed by react for us.
It is always preferable to use event listeners this way if possible because we don’t have to worry about cleaning them up.
Manually bound event listeners
There’s an issue with the above example. If our focus isn’t inside of the modal (This is quite common. Focus could be inside on the document if nothing in particular is focused). The handleEscape
method will not be triggered. This means the modal won’t close. In this case we have to add the event listener on the document to produce the expected behaviour.
Let’s see what a naive implementation of this might look like.
import React from "react";
interface IProps {}
class Modal extends React.Component<IProps> {
public render() {
<div className="modal" onKeyDown={this.handleEscape}>
{this.state.wasEscapePressed ? "Escape Pressed!" : "Hello world."}
</div>
}
/**
* We need to listen to all escape keys to determine whether or not we close.
*/
public componentDidMount() {
document.addEventListener("keydown", this.handleEscape);
}
private handleEscape = (event: React.KeyboardEvent) => {
if(event.key === "escape") {
this.closeModal();
}
}
private closeModal() {
// Do something to close the modal.
}
}
This works perfectly! Now our keyboard listener works all the time. Every thing is fine and dandy until, suddenly you see something like the following in your console:
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op,
but it indicates a memory leak in your application. To fix, cancel all subscriptions and
asynchronous tasks in the componentWillUnmount method.
This happens because we are creating a subscription (event listener) in our component and not keeping removing. We’ve added a reference to this
to a method existing outside of our components lifetime and did not clean it up.
The fix is simple. Implementing the componentWillUnmount()
method. This is called before the component is unmounting and is the designated time to cancel async tasks or listeners.
class Modal extends React.Component<IProps> {
// ...
public componentDidMount() {
document.addEventListener("keydown", this.handleEscape);
}
public componentWillUnmount() {
document.removeEventListener("keydown", this.handleEscape);
}
//...
}
Simply put, you componentWillUnmount
should do the opposite of componentDidMount()
.