Skip to content

dispose() on HybridObject prop crashes Fabric scheduler in DEV mode #1296

Description

@mfazekas

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:

  1. Cannot get hybrid property HybridMemoryHungryObjectSpec.label - this's NativeState is null, did you accidentally call dispose() on this object?
  2. 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 (logComponentRenderaddObjectDiffToProperties) 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions