React 17 removes event pooling in the modern browsers


The Event handlers in React applications are passed instances of SyntheticEvent, a cross-browser wrapper around the browser’s native event.

Event Pooling

The SyntheticEvent objects are pooled.

This means, when an event is triggered, React takes an instance from the pool, populates its properties and, reuses it.

To assure consistent usage of the pooled events, React nullifies the properties of synthetic events right after executing an event handler.

This method saves allocations during high firing events, but adds a bit of overhead in “releasing”, “destroying” and “reusing” instances.

Though Event Pooling was built to increase the performance it didn’t improve the performance in modern browsers. It also confused developers. For example, not being to access eventx.target in the setState updater.

So the React team decided to remove event pooling.

Before

Let’s checkout a simple example of input element and its event handler.

export class App extends React.Component {
  constructor() {
    super();
    this.state = { text: '' };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
    console.log(event.target.value);
    this.setState(() => ({
      text: event.target.value
    }));
  }
  render() {
    return (
      <div>
        <span>Text: </span>
        <input onChange={this.handleChange} value={this.state.text} />
      </div>
    );
  }
}

When we type something in the input box, we get below the error and warning

TypeError: Cannot read property 'value' of null`

Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property `target` on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().

This is because React reused the event objects between different events for performance in old browsers, and set all event fields to null in between them.

With React 16 and earlier, we have to call event.persist() to properly use the event, or read the property we need.

handleChange(event) {
    console.log(event.target.value);
    event.persist();
    this.setState(() => ({
      text: event.target.value
    }));
  }

After

In React 17, the same code works as expected allowing us to fetch event.target.value without calling event.persist().

The old event pooling optimization has been fully removed, so we can read the event fields whenever we need them.

export class App extends React.Component {
  constructor() {
    super();
    this.state = { text: '' };
    this.handleChange = this.handleChange.bind(this);
  }
  handleChange(event) {
    console.log(event.target.value);
    this.setState(() => ({
      text: event.target.value
    }));
  }
  render() {
    return (
      <div>
        <span>Text: </span>
        <input onChange={this.handleChange} value={this.state.text} />
      </div>
    );
  }
}

Note: event.persist() is still available on the React event object, but now it doesn’t do anything.

Check out the pull request to learn more.