Skip to content

SyntheticEvent Pooling Warning in React 16.x when using applyReactInVue #187

@ksh5324

Description

@ksh5324

SyntheticEvent Pooling Warning in React 16.x when using applyReactInVue

When using React components inside a Vue environment via applyReactInVue,
a warning occurs in React 16.x environments if a SyntheticEvent is passed to Vue
and later accessed again in an asynchronous execution context.

Warning: This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the method `timeStamp` on a released/nullified synthetic event.

This issue is not caused by a specific component or an external library.

Instead, it occurs because

veaury’s current event-passing behavior
exposes React SyntheticEvent objects outside the React layer into Vue

which structurally allows them to escape the React event lifecycle.

This document explains:

  1. Why this warning is reproducible only in React 16.x
  2. Under which scenarios the problem occurs
  3. The workaround we applied inside veaury

Environment

This warning occurs under the following conditions:

  • Vue 3
  • veaury applyReactInVue
  • React 16.x (SyntheticEvent pooling is aggressively enabled)
  • A SyntheticEvent is
    • passed to the Vue layer, and
    • later accessed again in an async / delayed execution flow

⚠ In React 17 / 18 the same code rarely produces this warning
due to differences in pooling behavior and release timing.

Therefore, this issue is relevant specifically to React 16.x users.


Problem Description

Currently, applyReactInVue creates React event handlers like:

<Component {...this.$attrs} // onClick / onInput / onChange ... {...otherProps} />

meaning Vue $attrs are passed directly to the React component.

In this setup:

  1. React creates a SyntheticEvent instance
  2. That object is passed to the Vue event handler
  3. Vue code later accesses the event again inside an async callback

Under React 16.x pooling rules:

the SyntheticEvent has already been released back to the pool,
but the Vue layer continues to reference it

This triggers the warning:

SyntheticEvent is reused / nullified

In other words:

a SyntheticEvent whose lifecycle has already ended inside React
becomes accessible outside the framework boundary (Vue layer)

which is the root cause of this issue.


Reproduction Example

Vue

<!-- src/views/TestReactPooling.vue -->
<template>
  <div style="padding: 20px">
    <h1>React SyntheticEvent Pooling Test (Vue3 + veaury)</h1>
    <p>Click the button and the Vue handler will read e.timeStamp asynchronously.</p>

    <TestButton @click="handleClick" />
  </div>
</template>

<script setup>
import { applyReactInVue } from '../../lib/vueinReact'
import TestButtonReact from '../../react_app/TestButton.jsx'

// Wrap with veaury
const TestButton = applyReactInVue(TestButtonReact)

// Vue receives a SyntheticEvent here
const handleClick = (e) => {
  // Synchronous access: still valid
  console.log('[sync] e.timeStamp:', e.timeStamp)

  // Asynchronous access
  setTimeout(() => {
    // React 16.x triggers pooling warning here:
    // "Warning: This synthetic event is reused for performance reasons..."
    console.log('[async] e.timeStamp:', e.timeStamp)
  }, 0)
}
</script>

React

// src/react_app/TestButton.jsx
import React from 'react';

class TestButton extends React.Component {
  // Compatible with React 16.4 — class fields not required
  handleClick(e) {
    // Pass SyntheticEvent directly to Vue
    if (this.props.onClick) {
      this.props.onClick(e);
    }
  }

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

export default TestButton;

Root Cause Summary

In React 16.x:

  • SyntheticEvent objects are returned to the pool after the event handler completes
  • Accessing them afterward triggers warnings

However, the current veaury event-passing behavior:

  • forwards the SyntheticEvent object
  • directly to the Vue layer and userland code

Developers on the Vue side have no indication that the object is a SyntheticEvent
and do not have an opportunity to call event.persist().

Therefore:

passing SyntheticEvent objects across framework boundaries
is incompatible with the React 16.x event lifecycle.


Applied Fix (Workaround)

In our project, we resolved this by:

  1. Calling event.persist() before leaving the React handler
    to guarantee the event is not pooled
  2. Passing only nativeEvent (with fallback) to the Vue layer
    instead of exposing the SyntheticEvent object itself
__veauryWrapReactEventListener__(fn) {  
  if (typeof fn !== 'function') return fn;    
  return (e, ...rest) => {     
    // Prevent SyntheticEvent pooling in React 16.x
    if (e && typeof e.persist === 'function') {
      e.persist();
    }
    // Only pass nativeEvent to the Vue layer
    const native = e?.nativeEvent ?? e; 
    return fn(native, ...rest); 
  }; 
}

We also automatically applied this wrapper
to event handlers passed via $attrs.

const attrsWithWrappedEvents = {}; 
Object.keys(this.$attrs || {}).forEach((key) => { 
  const val = this.$attrs[key]; 
  if (key.startsWith('on') && typeof val === 'function') {  
    attrsWithWrappedEvents[key] =  
    this.__veauryWrapReactEventListener__(val); 
  } else {  
    attrsWithWrappedEvents[key] = val; 
  }
});

Result

  • React 16.x
    • SyntheticEvent pooling warnings no longer occur
  • React 17+
    • Behavior remains identical, no side effects observed

Scope of Impact

  • Affects React 16.x only
  • Behavior is backward-safe
  • No behavior change in React 17+ environments
  • No API changes for veaury users

Proposal

In the current architecture:

  • React event objects may propagate
  • beyond React and into Vue or external user code

which leads to pooling warnings in React 16.x environments.

We propose the following improvement:

  • when forwarding events inside veaury

    • call event.persist()
    • pass only nativeEvent or a cloned event object to Vue

In short:

SyntheticEvent objects should not be exposed directly
across framework boundaries by default.

This approach would allow veaury to maintain compatibility
with the React 16.x event lifecycle while remaining safe for newer versions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions