pmndrs / pmndrs/react-spring

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

Open
#2,533 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
29.1k
Forks
1.2k
PR merge metrics
No merged PRs in 30d

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at createHost in @react-spring/animated, especially the animated() cache path, and reproduce with @react-spring/native under Hermes using the stated Metro settings. Verify that lazy loading no longer throws for non-extensible View, Text, and Image host components and that useSpring remains available after loading.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
mobile
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.