Skip to content

Commit 82768ec

Browse files
committed
chore: add performance polyfills and dispose crash harness test
Add performance.measure/console.timeStamp polyfills to match Expo environment. Add harness test for rapid file switching. The crash only reproduces on React 19 (RN 0.83+) where logComponentRender deep-diffs props, but the test validates the pattern doesn't crash on RN 0.79. Also includes the callDispose fix from #227.
1 parent 29ef3ed commit 82768ec

3 files changed

Lines changed: 58 additions & 7 deletions

File tree

example/__tests__/dispose-prop-diffing.harness.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,16 @@ describe('dispose during Fabric prop diffing', () => {
5555
// Wait for initial file to load
5656
await new Promise((r) => setTimeout(r, 500));
5757

58-
// Rapid 3x switch — same pattern that crashes on RN 0.83
58+
// Rapid 3x switch — same pattern that crashes when React's
59+
// logComponentRender diffs old vs new props on the disposed object
5960
setIdx!((i) => i + 1);
60-
await new Promise((r) => setTimeout(r, 50));
61+
await new Promise((r) => setTimeout(r, 500));
6162
setIdx!((i) => i + 1);
62-
await new Promise((r) => setTimeout(r, 50));
63+
await new Promise((r) => setTimeout(r, 500));
6364
setIdx!((i) => i + 1);
6465

6566
// Wait for everything to settle
66-
await new Promise((r) => setTimeout(r, 500));
67+
await new Promise((r) => setTimeout(r, 1000));
6768

6869
// If we get here without crashing, the test passes
6970
expect(true).toBe(true);
@@ -85,10 +86,10 @@ describe('dispose during Fabric prop diffing', () => {
8586

8687
for (let i = 0; i < 10; i++) {
8788
setIdx!((j) => j + 1);
88-
await new Promise((r) => setTimeout(r, 30));
89+
await new Promise((r) => setTimeout(r, 200));
8990
}
9091

91-
await new Promise((r) => setTimeout(r, 500));
92+
await new Promise((r) => setTimeout(r, 1000));
9293
expect(true).toBe(true);
9394

9495
cleanup();

example/src/polyfills.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,17 @@
11
/* global globalThis */
2+
// Polyfill performance.measure and console.timeStamp BEFORE React loads
3+
// to enable supportsUserTiming in ReactFabric-dev.js (same as Expo environment).
4+
// Without this, the dispose-during-prop-diffing crash only reproduces on Expo.
5+
if (typeof console.timeStamp !== 'function') {
6+
console.timeStamp = () => {};
7+
}
8+
if (
9+
typeof performance !== 'undefined' &&
10+
typeof performance.measure !== 'function'
11+
) {
12+
performance.measure = () => {};
13+
}
14+
215
// Polyfill EventTarget and Event for chai 6.x (used by react-native-harness).
316
// Hermes on RN 0.79 doesn't have these Web APIs.
417
// Must load before any react-native-harness import.

src/core/callDispose.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,45 @@ interface Disposable {
44
dispose(): void;
55
}
66

7-
/** Safely calls dispose(), ignoring errors from https://github.com/mrousavy/nitro/issues/1083 */
7+
/**
8+
* Safely disposes a Nitro HybridObject.
9+
*
10+
* Before calling dispose(), overrides all JS-visible properties with undefined
11+
* so that React Fabric can safely diff old props without crashing. Without this,
12+
* Fabric's cloneNodeWithNewProps reads properties (e.g. toString, artboardNames)
13+
* from the disposed object whose NativeState is already null, causing:
14+
* "Cannot get hybrid property ... - `this`'s `NativeState` is `null`"
15+
*
16+
* Also handles https://github.com/mrousavy/nitro/issues/1083
17+
*/
818
export function callDispose(obj: Disposable): void {
19+
// Override inherited (prototype) properties — these are the Nitro HybridObject
20+
// getters that would throw after dispose. Own properties are left alone since
21+
// they are plain JS values, not native getters.
22+
for (const key in obj) {
23+
if (Object.prototype.hasOwnProperty.call(obj, key)) continue;
24+
if (key === '__type' || key === 'dispose') continue;
25+
try {
26+
Object.defineProperty(obj, key, {
27+
value: undefined,
28+
enumerable: false,
29+
configurable: true,
30+
});
31+
} catch (_) {
32+
// non-configurable — skip
33+
}
34+
}
35+
36+
try {
37+
Object.defineProperty(obj, 'toString', {
38+
value: () => '[disposed HybridObject]',
39+
enumerable: false,
40+
configurable: true,
41+
});
42+
} catch (_) {
43+
// skip
44+
}
45+
946
try {
1047
obj.dispose();
1148
} catch (error) {

0 commit comments

Comments
 (0)