Skip to content

animated() throws 'cannot add a new property' on non-extensible React Native host components (Hermes) #2533

Description

@AndreiCalazans

Bug

@react-spring/animated's createHost caches animated wrappers by writing directly to the component object:

Component[cacheKey] = withAnimated(Component, hostConfig)

On Hermes, React Native host components (View, Text, Image) become non-extensible after their first JSX render. In strict mode this throws TypeError: cannot add a new property, which crashes the module initialization.

Reproduction

  • React Native with Hermes
  • Metro config: inlineRequires: true + experimentalImportSupport: true
  • Any component that uses @react-spring/native loaded lazily (inside a render)

With inlined requires, @react-spring/native is first required during a component render — by that point Hermes has already rendered the host components via JSX elsewhere and locked their shapes. createHost runs at module scope, iterates { View, Text, Image }, and throws on the first Component[cacheKey] = ... assignment.

Metro's guardedLoadModule swallows the error and returns undefined for the module, causing a subsequent TypeError: Cannot read property 'useSpring' of undefined.

Call stack (from device)

animated          react-spring_animated.development.cjs:356
<anonymous>       react-spring_animated.development.cjs:365
eachProp          react-spring_shared.development.cjs:120
createHost        react-spring_animated.development.cjs:361
<global>          react-spring_native.development.cjs:90
loadModuleImplementation  require.js:285
guardedLoadModule

Fix

Use the direct property write as the fast path, catch the TypeError, and fall back to a module-level WeakMap:

const fallbackCache = new WeakMap<object, any>()

// inside animated():
let cached = Component[cacheKey] ?? fallbackCache.get(Component)
if (!cached) {
  cached = withAnimated(Component, hostConfig)
  try {
    Component[cacheKey] = cached
  } catch {
    // non-extensible component (e.g. Hermes host component)
  }
  fallbackCache.set(Component, cached)
}
Component = cached

PR with fix: https://github.com/AndreiCalazans/react-spring/pull/new/fix/non-extensible-component-cache

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions