Skip to content

fix(core): leave React's factory alone once the app declared its runtime - #19

Merged
Fiona2016 merged 1 commit into
publishfrom
fix/dont-touch-react-factory-when-runtime-declared
Aug 27, 2026
Merged

fix(core): leave React's factory alone once the app declared its runtime#19
Fiona2016 merged 1 commit into
publishfrom
fix/dont-touch-react-factory-when-runtime-declared

Conversation

@Fiona2016

Copy link
Copy Markdown
Collaborator

Fixes the startup crash that #18 did not fix. Confirmed on a simulator release build, with the pre-fix build as the negative control.

What actually causes it

Patching React.createElement. Nothing subtler.

Bisected on a release build of a nativewind app:

build result
0.1.4 as published 💥 hermes::vm::GCBase::oomabort, SIGABRT on the JS thread
#18 (accessor skipped, read/write on the same object) 💥 same crash
both React-level patches skipped (createElement + memo) ✅ starts, reports views, records taps
only createElement skipped, memo still patched ✅ starts

So memo is not involved, and neither is how the property gets replaced. Replacing it at all routes every createElement call in the process — React's own internals included — through machinery the host wrote for its own calls. On a styling library that owns the element factory, that ends with the heap growing until Hermes aborts.

The two changes in #18 are correct on their own terms and stay. They just treat a different problem.

The fix

Skip the React.createElement patch once jsxRuntimes tells us where the app's elements come from. Under the automatic JSX transform such an app never calls React.createElement anyway — the declared runtime is what records the taps — so no instrumentation is lost. An app that declares nothing keeps the previous behaviour.

Evidence

Negative control (0.1.4 as published, same app, same config including jsxRuntimes):

Thread: com.facebook.react.JavaScript   Signal: SIGABRT
hermes::vm::GCBase::oom(std::error_code)
hermes::vm::HadesGC::OldGen::alloc(unsigned int)

With the fix, the same app starts and reports:

t_views    ApplicationLaunch, /, /login
t_actions  tap  RCTView  /login        ← taps recorded

Tests

  • React's factory is untouched when a runtime was declared, and the declared runtime is still instrumented
  • React's factory is still patched when nothing was declared

Full suite green (885), lerna run prepare across all 9 projects, lint clean.

Patching React.createElement is what crashes an app whose styling library owns the
element factory. Measured on a release build of a nativewind app: the heap grows without
bound until Hermes aborts during startup. Bisected against a build with only that patch
removed - `memo` stays patched there and the app survives - so the factory patch is the
whole of it.

Two earlier attempts treated this as a matter of replacing the property more carefully:
skip it when it is an accessor, and read it back through the same object the write lands
on. Both are correct on their own terms and both are kept, but neither stops the crash.
The problem is not how the property is replaced. It is that replacing it at all routes
every createElement call in the process - React's internals included - through machinery
the host wrote for its own calls.

So the patch is now skipped once `jsxRuntimes` says where the app's elements actually
come from. Under the automatic JSX transform such an app never calls React.createElement
anyway, so nothing is instrumented that was not already: the declared runtime is what
records the taps. An app that declares nothing still gets the old behaviour.

Verified on a simulator release build: with the declared runtime instrumented and the
factory left alone, the app starts, reports views, and records taps - `t_actions` rows
of type `tap`, and `view_action_count` on the view they belong to. The pre-fix build is
the negative control, and it aborts before any of that.
@Fiona2016
Fiona2016 merged commit e4c5e58 into publish Aug 27, 2026
1 check passed
@Fiona2016 Fiona2016 mentioned this pull request Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant