Skip to content

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

@AndreiCalazans

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions