Event pooling
One challenge with wrapping native event instances is that this can cause performance issues. Every synthetic event wrapper that's created will also need to be garbage collected at some point, which can be expensive in terms of CPU time.
Note
When the garbage collector is running, none of your JavaScript code is able to run. This is why it's important to be memory efficient; frequent garbage collection means less CPU time for code that responds to user interactions.
For example, if your application only handles a few events, this wouldn't matter much. But even by modest standards, applications respond to many events, even if the handlers don't actually do anything with them. This is problematic if React constantly has to allocate new synthetic event instances.
React deals with this problem by allocating a synthetic instance pool. Whenever an event is triggered, it takes an instance from the pool and populates its properties. When the event handler has finished running, the synthetic event instance is released back into the pool, as shown here:
This prevents the garbage collector from running frequently when a lot of events are triggered. The pool keeps a reference to the synthetic event instances, so they're never eligible for garbage collection. React never has to allocate new instances either.
However, there is one gotcha that you need to be on the lookout for. It involves accessing the synthetic event instances from asynchronous code in your event handlers. This is an issue because as soon as the handler has finished running, the instance goes back into the pool. When it goes back into the pool, all of its properties are cleared. Here's an example that shows how this can go wrong:
import React, { Component } from 'react'; // Mock function, meant to simulate fetching // data asynchronously from an API. function fetchData() { return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); } export default class MyButton extends Component { onClick(e) { // This works fine, we can access the DOM element // through the "currentTarget" property. console.log('clicked', e.currentTarget.style); fetchData().then(() => { // However, trying to access "currentTarget" // asynchronously fails, because it's properties // have all been nullified so that the instance // can be reused. console.log('callback', e.currentTarget.style); }); } render() { return ( <button onClick={this.onClick}> {this.props.children} </button> ); } }
As you can see, the second console.log()
is attempting to access a synthetic event property from an asynchronous callback that doesn't run until the event handler completes, which causes the event to empty its properties. This results in a warning and an undefined value.
Note
The aim of this example is to illustrate how things can break when you write asynchronous code that interacts with events. Just don't do it!