@samitouri / QOS-React-2 / commits / a48e9e3f10

[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.

Rubén Norte committed Feb 26, 2026 at 15:51 UTC a48e9e3f10fed06c813399ccae8a28db7dd76683
2 files changed +148 -1
packages/react-native-renderer/src/__tests__/ReactFabric-test.internal.js
+132
@@ -1153,6 +1153,138 @@ describe('ReactFabric', () => {
1153 expect.assertions(6);
1154 });
1155
1156 + it('propagates timeStamps from native events and sets defaults', async () => {
1157 + const View = createReactNativeComponentClass('RCTView', () => ({
1158 + validAttributes: {
1159 + id: true,
1160 + },
1161 + uiViewClassName: 'RCTView',
1162 + directEventTypes: {
1163 + topTouchStart: {
1164 + registrationName: 'onTouchStart',
1165 + },
1166 + topTouchEnd: {
1167 + registrationName: 'onTouchEnd',
1168 + },
1169 + },
1170 + }));
1171 +
1172 + function getViewById(id) {
1173 + const [reactTag, , , , instanceHandle] =
1174 + nativeFabricUIManager.createNode.mock.calls.find(
1175 + args => args[3] && args[3].id === id,
1176 + );
1177 +
1178 + return {reactTag, instanceHandle};
1179 + }
1180 +
1181 + const ref1 = React.createRef();
1182 + const ref2 = React.createRef();
1183 + const ref3 = React.createRef();
1184 +
1185 + const explicitTimeStampCamelCase = 'explicit-timestamp-camelcase';
1186 + const explicitTimeStampLowerCase = 'explicit-timestamp-lowercase';
1187 + const performanceNowValue = 'performance-now-timestamp';
1188 +
1189 + jest.spyOn(performance, 'now').mockReturnValue(performanceNowValue);
1190 +
1191 + await act(() => {
1192 + ReactFabric.render(
1193 + <>
1194 + <View
1195 + ref={ref1}
1196 + id="default"
1197 + onTouchEnd={event => {
1198 + expect(event.timeStamp).toBe(performanceNowValue);
1199 + }}
1200 + />
1201 + <View
1202 + ref={ref2}
1203 + id="explicitTimeStampCamelCase"
1204 + onTouchEnd={event => {
1205 + expect(event.timeStamp).toBe(explicitTimeStampCamelCase);
1206 + }}
1207 + />
1208 + <View
1209 + ref={ref2}
1210 + id="explicitTimeStampLowerCase"
1211 + onTouchEnd={event => {
1212 + expect(event.timeStamp).toBe(explicitTimeStampLowerCase);
1213 + }}
1214 + />
1215 + </>,
1216 + 1,
1217 + null,
1218 + true,
1219 + );
1220 + });
1221 +
1222 + const [dispatchEvent] =
1223 + nativeFabricUIManager.registerEventHandler.mock.calls[0];
1224 +
1225 + dispatchEvent(getViewById('default').instanceHandle, 'topTouchStart', {
1226 + target: getViewById('default').reactTag,
1227 + identifier: 17,
1228 + touches: [],
1229 + changedTouches: [],
1230 + });
1231 + dispatchEvent(getViewById('default').instanceHandle, 'topTouchEnd', {
1232 + target: getViewById('default').reactTag,
1233 + identifier: 17,
1234 + touches: [],
1235 + changedTouches: [],
1236 + // No timeStamp property
1237 + });
1238 +
1239 + dispatchEvent(
1240 + getViewById('explicitTimeStampCamelCase').instanceHandle,
1241 + 'topTouchStart',
1242 + {
1243 + target: getViewById('explicitTimeStampCamelCase').reactTag,
1244 + identifier: 17,
1245 + touches: [],
1246 + changedTouches: [],
1247 + },
1248 + );
1249 +
1250 + dispatchEvent(
1251 + getViewById('explicitTimeStampCamelCase').instanceHandle,
1252 + 'topTouchEnd',
1253 + {
1254 + target: getViewById('explicitTimeStampCamelCase').reactTag,
1255 + identifier: 17,
1256 + touches: [],
1257 + changedTouches: [],
1258 + timeStamp: explicitTimeStampCamelCase,
1259 + },
1260 + );
1261 +
1262 + dispatchEvent(
1263 + getViewById('explicitTimeStampLowerCase').instanceHandle,
1264 + 'topTouchStart',
1265 + {
1266 + target: getViewById('explicitTimeStampLowerCase').reactTag,
1267 + identifier: 17,
1268 + touches: [],
1269 + changedTouches: [],
1270 + },
1271 + );
1272 +
1273 + dispatchEvent(
1274 + getViewById('explicitTimeStampLowerCase').instanceHandle,
1275 + 'topTouchEnd',
1276 + {
1277 + target: getViewById('explicitTimeStampLowerCase').reactTag,
1278 + identifier: 17,
1279 + touches: [],
1280 + changedTouches: [],
1281 + timestamp: explicitTimeStampLowerCase,
1282 + },
1283 + );
1284 +
1285 + expect.assertions(3);
1286 + });
1287 +
1288 it('findHostInstance_DEPRECATED should warn if used to find a host component inside StrictMode', async () => {
1289 const View = createReactNativeComponentClass('RCTView', () => ({
1290 validAttributes: {foo: true},
packages/react-native-renderer/src/legacy-events/SyntheticEvent.js
+16 -1
@@ -11,6 +11,21 @@ import assign from 'shared/assign';
11
12 const EVENT_POOL_SIZE = 10;
13
14 +let currentTimeStamp = () => {
15 + // Lazily define the function based on the existence of performance.now()
16 + if (
17 + typeof performance === 'object' &&
18 + performance !== null &&
19 + typeof performance.now === 'function'
20 + ) {
21 + currentTimeStamp = () => performance.now();
22 + } else {
23 + currentTimeStamp = () => Date.now();
24 + }
25 +
26 + return currentTimeStamp();
27 +};
28 +
29 /**
30 * @interface Event
31 * @see http://www.w3.org/TR/DOM-Level-3-Events/
@@ -26,7 +41,7 @@ const EventInterface = {
41 bubbles: null,
42 cancelable: null,
43 timeStamp: function (event) {
29 - return event.timeStamp || Date.now();
44 + return event.timeStamp || event.timestamp || currentTimeStamp();
45 },
46 defaultPrevented: null,
47 isTrusted: null,