
Stop Using CustomEvent
I see a lot web developers out there firing CustomEvent
s from their component code. So much that it seems like many developers think that CustomEvent
is the only way for their code to fire custom (with a little "c") events, and maybe even the only way to fire their own events at all.
It's understandable. It's right in the name: "Custom" event. It feels like the designated tool for the job. It even sounds like it goes right along with "custom element". But I always tell developers to not use CustomEvent
. There's no reason for it. Why?
What's wrong with CustomEvent?
First, let me be honest here: using CustomEvent
isn't the end of the world. In terms of bad practices in web developer, this is hardly a nine alarm fire. It's barely even leaving a candle burning. CustomEvent
works, and using it won't harm the users that matter most: application end users.
That said, this is apparently something I rant a bit in various web dev circles, so it's probably better I put my arguments down in one place.
CustomEvent shouldn't exist!
CustomEvent
only exists because subclassing native classes wasn't well-supported when it was introduced. Adding a details object via the event init options was the platform-blessed way to add a custom payload to an Event. I presume you could also patch properties onto any Event
instance, but maybe some quirk of the spec or IE meant that you couldn't rely on the same Event
instance arriving at every listener.
Then ES2015 came out and suddenly we could subclass native classes like Array
, HTMLElement
, and Event
! Had ES6 (what we used to call ES2015) come out earlier, CustomEvent
would never had happened. I've heard this directly from browser engineers responsible for the DOM APIs at that time.
CustomEvent is a leaky implementation detail
When you fire a CustomEvent
, you're forcing the user (the developer user) of your component to deal with the detail
property. They have to know that the data they need is tucked away in this arbitrary container.
// The consumer experience with CustomEvent
someElement.addEventListener('my-event', (e: CustomEvent<MyEventDetail>) => {
// Why the extra step?
const { foo, bar } = e.detail;
// ...
});
This is a leaky abstraction. Your users shouldn't have to care about the implementation detail that you chose CustomEvent
. They should be interacting with an event that feels as native and intuitive as a MouseEvent
or a KeyboardEvent
.
What to use instead of CustomEvent
So, what should you use instead, and why?
Obviously, since CustomEvent
was added due to a lack of subclassing, the main alternative is to subclass the native Event
class. But I'll get into that in one bit. First, let me suggest alternatives that don't involve subclassing: using a plain Event
or using a built-in event subclass.
Do you really need custom data?
The Event
constructor takes an event name and options (for bubbling, composed, etc). If you don't need additional data with your event, you can just new up a plain Event
. And I think that many CustomEvent
s don't actually need their detail
object.
Consider that many platform events and simply a notification that something happened, and a reference back to the thing it happened to: the event target. For instance, when an <input>
element fires a 'input'
event, the event doesn't carry a new value. Instead you look at the event target and get the current value directly from that.
The nice thing about getting data this way is that it's robust against the value changing after the event is handled. You can take code that works synchronously with the 'input'
event, wrap it in a debounce, and it'll still work.
Payloadless events are also more general and reusable. You could fire an 'input'
event from an element that has a different data type, or multiple pieces of relevant data, without changing the event. Many platform events are payloadless and just a plain Event
instance, like 'input'
, 'change'
, 'select'
, 'load'
...
So, first, I would consider if you can rely on event.target
instead of a payload.
Now, this is a subjective choice, because many people find having data right on the event to be easier to use. And maybe you do need to record the data as it was when the event was fired. These are fine reasons to use a custom payload.
Also, you may want to subclass Event
even if you don't have a custom payload, for some of the other benefits I mention below.
Is there a native event that meets your needs?
Second, I would consider if a native event has the semantics you need. This could be a native event that uses a generic Event
instance, or a native subclass of Event
. The advantage of using a built-in event name and class is that your users and their tools may already be familiar with that event.
For example, when a component loads something, you can fire a load
event. Or if you want to report an error from a component you can fire an ErrorEvent
. If your component has open and close states, consider firing a ToggleEvent
.
Just make sure that your event's semantics match the platform event's semantics exactly. They should be fired for the same reasons, at the same timing, with the same options (eg, bubbles).
Subclassing Event
Since we can subclass Event
when we need to, this is my main and most general recommended alternative to CustomEvent
. Event
subclasses are better all-around.
This is an example of my basic pattern for subclassing Event
:
/**
* An event that's fired when foo happens.
*/
export class MyEvent extends Event {
static readonly eventName = 'my-event';
readonly foo: number;
readonly bar: string;
constructor(foo: number, bar: string) {
super(MyEvent.eventName, { bubbles: true, composed: true });
this.foo = foo;
this.bar = bar;
}
}
Look at what this gives us.
First, it's easier to use and plain nicer. The event's data is directly on the event object. It feels just like a native event.
// The consumer experience with a proper Event subclass
someElement.addEventListener(MyEvent.eventName, (e: MyEvent) => {
// So much cleaner.
const { foo, bar } = e;
// ...
});
Second, the event subclass serves as its own documentation. The class definition is a natural place to use JSDoc to explain what the event is for and what its properties mean.
Third, and the most important for program correctness, it provides a single source of truth for the event's creation. The constructor ensures that every time MyEvent
is instantiated and dispatched it has the same name and the same options, like bubbles
and composed
. You could achieve something similar with a factory function for CustomEvent
, but at that point, you're just writing a poor man's class.
Finally, the Event
subclass is its own type. This is a huge win for TypeScript users. Instead of fumbling with CustomEvent<MyDetailType>
, you just use the event class itself as the type. It's simpler and more accurate.
In many cases it even makes sense to register the event type globally too:
declare global {
interface GlobalEventHandlersEventMap {
'my-event': MyEvent;
}
}
Then your users will automatically get correctly-typed event handlers when using addEventListener()
. Of course, you can do this with CustomEvent
too, so should do this in the same cases, either way.
Further reading
- Thomas Broyer recently updated MDN to mention subclassing Event on the Creating and triggering events page!
- Burton Smith is writing on the topic and has a piece on better typing CustomEvent. I hear there is a Part 2 coming soon that talks about subclassing.