What happened?
We have HybridObjects that hold large native resources (GPU textures, image buffers) and we need to call dispose() eagerly in useEffect cleanup rather than waiting for Hermes GC to eventually free them. When a HybridObject that was passed as a prop to a HybridView is disposed, the app crashes and becomes completely unresponsive.
We see these errors:
Cannot get hybrid property HybridMemoryHungryObjectSpec.label - this's NativeState is null, did you accidentally call dispose() on this object?
Should not already be working. (×2)
After these errors the app is frozen — no taps, no renders, no recovery until bundle reload.
This happens in Expo SDK 55 (RN 0.83.x, Fabric, DEV mode). We could also reproduce it in the nitro example app by polyfilling performance.measure and console.timeStamp before React loads (Expo provides these natively).
The error comes from React's DEV-mode profiling (logComponentRender → addObjectDiffToProperties) which deeply diffs old vs new component props using for...in. After dispose() nulls NativeState, the deep diff iterates the disposed HybridObject's properties and throws.
Related: #1083 — when the HybridObject IS frozen by React (deepFreezeAndThrowOnMutationInDev), dispose() fails with "failed to define internal native state property" instead. Both issues stem from dispose() + HybridView props not playing well together.
Workaround
Libraries can work around this by shadowing prototype properties from JS before calling dispose():
function callDispose(obj: Disposable): void {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) continue;
if (key === '__type' || key === 'dispose') continue;
try {
Object.defineProperty(obj, key, {
value: undefined, enumerable: false, configurable: true,
});
} catch (_) {}
}
obj.dispose();
}
See rive-app/rive-nitro-react-native#227 for a full implementation.
Proposed nitro-level fix: #1298
Reproduceable Code
Reproduction branch: https://github.com/mfazekas/nitro/tree/test/dispose-fabric-crash
Diff from main: main...mfazekas:nitro:test/dispose-fabric-crash
Key files:
example/src/screens/DisposeReproScreen.tsx — the repro screen
example/src/polyfillUserTiming.js — polyfills to simulate Expo environment
example/index.js — loads polyfill before React
function useMemoryHungryObject(sizeInBytes: number, label: string) {
const [obj, setObj] = useState<MemoryHungryObject | undefined>()
useEffect(() => {
const created = factory.create(sizeInBytes, label)
setObj(created)
return () => { created.dispose() } // ← dispose during cleanup
}, [sizeInBytes, label])
return obj
}
// View wrapper (common pattern in Expo / user code)
function WrappedMemoryHungryView(props) {
return <MemoryHungryView {...props} />
}
function HungryBox({ size, label }) {
const obj = useMemoryHungryObject(size, label)
return obj && <WrappedMemoryHungryView object={obj} style={{ flex: 1 }} />
}
Relevant log output
LOG [DisposeRepro] auto-triggering rapid 3x
LOG [dispose] Object-A
LOG [dispose] Object-B
ERROR [Error: Cannot get hybrid property `HybridMemoryHungryObjectSpec.label` - `this`'s `NativeState` is `null`, did you accidentally call `dispose()` on this object?]
ERROR [Error: Should not already be working.]
ERROR [Error: Should not already be working.]
Stack trace from Expo standalone:
addObjectDiffToProperties (ReactFabric-dev.js)
→ logComponentRender
→ commitPassiveMountOnFiber
→ flushPassiveEffects
Device
iPhone 16 Pro Simulator (iOS 18.6)
Nitro Modules Version
0.35.4
Nitrogen Version
0.35.4
Can you reproduce this issue in the Nitro Example app here?
Yes, I can reproduce the same issue in the Example app here
Additional information
What happened?
We have HybridObjects that hold large native resources (GPU textures, image buffers) and we need to call
dispose()eagerly inuseEffectcleanup rather than waiting for Hermes GC to eventually free them. When a HybridObject that was passed as a prop to a HybridView is disposed, the app crashes and becomes completely unresponsive.We see these errors:
Cannot get hybrid property HybridMemoryHungryObjectSpec.label - this's NativeState is null, did you accidentally call dispose() on this object?Should not already be working.(×2)After these errors the app is frozen — no taps, no renders, no recovery until bundle reload.
This happens in Expo SDK 55 (RN 0.83.x, Fabric, DEV mode). We could also reproduce it in the nitro example app by polyfilling
performance.measureandconsole.timeStampbefore React loads (Expo provides these natively).The error comes from React's DEV-mode profiling (
logComponentRender→addObjectDiffToProperties) which deeply diffs old vs new component props usingfor...in. Afterdispose()nullsNativeState, the deep diff iterates the disposed HybridObject's properties and throws.Related: #1083 — when the HybridObject IS frozen by React (
deepFreezeAndThrowOnMutationInDev),dispose()fails with "failed to define internal native state property" instead. Both issues stem fromdispose()+ HybridView props not playing well together.Workaround
Libraries can work around this by shadowing prototype properties from JS before calling
dispose():See rive-app/rive-nitro-react-native#227 for a full implementation.
Proposed nitro-level fix: #1298
Reproduceable Code
Reproduction branch: https://github.com/mfazekas/nitro/tree/test/dispose-fabric-crash
Diff from main: main...mfazekas:nitro:test/dispose-fabric-crash
Key files:
example/src/screens/DisposeReproScreen.tsx— the repro screenexample/src/polyfillUserTiming.js— polyfills to simulate Expo environmentexample/index.js— loads polyfill before ReactRelevant log output
Stack trace from Expo standalone:
Device
iPhone 16 Pro Simulator (iOS 18.6)
Nitro Modules Version
0.35.4
Nitrogen Version
0.35.4
Can you reproduce this issue in the Nitro Example app here?
Yes, I can reproduce the same issue in the Example app here
Additional information