[RN] Fix timeStamp property of SyntheticEvent in React Native (#35912)

## Summary

This fixes the semantics of the `timeStamp` property of events in React
Native.

Currently, most events just assign `Date.now()` (at the time of creating
the event object in JavaScript) as the `timeStamp` property. This is a
divergence with Web and most native platforms, that use a monotonic
timestamp for the value (on Web, the same timestamp provided by
`performance.now()`).

Additionally, many native events specify a timestamp in the event data
object as `timestamp` and gets ignored by the logic in JS as it only
looks at properties named `timeStamp` specifically (camel case).

This PR fixes both issues by:
1. Using `performance.now()` instead of `Date.now()` by default (if
available).
2. Checking for a `timestamp` property before falling back to the
default (apart from `timeStamp`).

## How did you test this change?

Added unit tests for verify the new behavior.
This commit is contained in:
Rubén Norte
2026-02-26 15:51:07 +00:00
committed by GitHub
parent baa92c6a84
commit 5e653d49a0
2 changed files with 148 additions and 1 deletions
@@ -1153,6 +1153,138 @@ describe('ReactFabric', () => {
expect.assertions(6);
});
it('propagates timeStamps from native events and sets defaults', async () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {
id: true,
},
uiViewClassName: 'RCTView',
directEventTypes: {
topTouchStart: {
registrationName: 'onTouchStart',
},
topTouchEnd: {
registrationName: 'onTouchEnd',
},
},
}));
function getViewById(id) {
const [reactTag, , , , instanceHandle] =
nativeFabricUIManager.createNode.mock.calls.find(
args => args[3] && args[3].id === id,
);
return {reactTag, instanceHandle};
}
const ref1 = React.createRef();
const ref2 = React.createRef();
const ref3 = React.createRef();
const explicitTimeStampCamelCase = 'explicit-timestamp-camelcase';
const explicitTimeStampLowerCase = 'explicit-timestamp-lowercase';
const performanceNowValue = 'performance-now-timestamp';
jest.spyOn(performance, 'now').mockReturnValue(performanceNowValue);
await act(() => {
ReactFabric.render(
<>
<View
ref={ref1}
id="default"
onTouchEnd={event => {
expect(event.timeStamp).toBe(performanceNowValue);
}}
/>
<View
ref={ref2}
id="explicitTimeStampCamelCase"
onTouchEnd={event => {
expect(event.timeStamp).toBe(explicitTimeStampCamelCase);
}}
/>
<View
ref={ref2}
id="explicitTimeStampLowerCase"
onTouchEnd={event => {
expect(event.timeStamp).toBe(explicitTimeStampLowerCase);
}}
/>
</>,
1,
null,
true,
);
});
const [dispatchEvent] =
nativeFabricUIManager.registerEventHandler.mock.calls[0];
dispatchEvent(getViewById('default').instanceHandle, 'topTouchStart', {
target: getViewById('default').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
});
dispatchEvent(getViewById('default').instanceHandle, 'topTouchEnd', {
target: getViewById('default').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
// No timeStamp property
});
dispatchEvent(
getViewById('explicitTimeStampCamelCase').instanceHandle,
'topTouchStart',
{
target: getViewById('explicitTimeStampCamelCase').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
},
);
dispatchEvent(
getViewById('explicitTimeStampCamelCase').instanceHandle,
'topTouchEnd',
{
target: getViewById('explicitTimeStampCamelCase').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
timeStamp: explicitTimeStampCamelCase,
},
);
dispatchEvent(
getViewById('explicitTimeStampLowerCase').instanceHandle,
'topTouchStart',
{
target: getViewById('explicitTimeStampLowerCase').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
},
);
dispatchEvent(
getViewById('explicitTimeStampLowerCase').instanceHandle,
'topTouchEnd',
{
target: getViewById('explicitTimeStampLowerCase').reactTag,
identifier: 17,
touches: [],
changedTouches: [],
timestamp: explicitTimeStampLowerCase,
},
);
expect.assertions(3);
});
it('findHostInstance_DEPRECATED should warn if used to find a host component inside StrictMode', async () => {
const View = createReactNativeComponentClass('RCTView', () => ({
validAttributes: {foo: true},
@@ -11,6 +11,21 @@ import assign from 'shared/assign';
const EVENT_POOL_SIZE = 10;
let currentTimeStamp = () => {
// Lazily define the function based on the existence of performance.now()
if (
typeof performance === 'object' &&
performance !== null &&
typeof performance.now === 'function'
) {
currentTimeStamp = () => performance.now();
} else {
currentTimeStamp = () => Date.now();
}
return currentTimeStamp();
};
/**
* @interface Event
* @see http://www.w3.org/TR/DOM-Level-3-Events/
@@ -26,7 +41,7 @@ const EventInterface = {
bubbles: null,
cancelable: null,
timeStamp: function (event) {
return event.timeStamp || Date.now();
return event.timeStamp || event.timestamp || currentTimeStamp();
},
defaultPrevented: null,
isTrusted: null,