fix(core): stop routing every createElement call through the host's factory - #18
Merged
Merged
Conversation
0.1.4 started redefining a factory that refused a plain assignment. That was the wrong reading of the failure. A host framework that exposes an element factory through a getter is not merely storing a function in that slot - it is managing the slot, and the value it hands out participates in bookkeeping of its own. Redefining it as a plain property does succeed, and from then on that bookkeeping operates on something the host no longer controls. On an app using a css-interop based styling library the result was the heap growing without bound until Hermes aborted during startup. So this is worse than what 0.1.3 did. There the assignment threw, which was crude but at least left the host intact; 0.1.4 removed the exception and replaced it with silent corruption. Not writable is not an obstacle to push harder against. It is the host saying the slot is taken, and the right response is to leave it alone and say so. A plain property that merely refuses writes carries no such logic behind it, so redefining that one stays. Nothing is lost by stepping back here: under the automatic JSX transform an app never calls React.createElement, so what actually instruments taps is the runtime it declares through `jsxRuntimes` - and those factories are plain properties. The one visible side effect, now documented, is that leaving `memo` alone costs the memoization of components whose onPress gets wrapped.
`const original = React.createElement` and the replacement that follows it were not talking about the same property. A host's Babel plugin can rewrite that member expression to its own factory - react-native-css-interop rewrites `React.createElement` to `ReactNativeCSSInterop.createInteropElement` in every file outside react and itself, the SDK's included - while the replacement, written as a computed access, is invisible to that plugin and lands on React's own property. So the SDK saved the host's wrapper and then replaced React's factory with something that calls it. From that point every createElement call in the process, React's own internals included, was routed through the host's wrapper: input it was never written to receive. On an app using such a library this shows up as the heap growing without bound until Hermes aborts during startup. Both sides now read through the module object with a computed access, which no plugin of that kind can rewrite, so `original` is what was actually replaced. The same applies to the saved value `stopTracking` restores from. Worth noting what this means for 0.1.3 and 0.1.4. In 0.1.3 both sides were member expressions: the read and the assignment were rewritten together, the assignment hit a getter-only property and threw, and the factory was never actually replaced. 0.1.4 changed the write to a computed access to stop that exception - and by doing so replaced the factory for the first time, with a mismatched original. The exception it removed was load-bearing.
This was referenced Aug 27, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes a regression introduced in 0.1.4. On an app using a css-interop based styling library (nativewind), 0.1.4 with
trackInteractions: truecrashes at startup — HermesGCBase::oom, heap growing without bound.The defect
These two lines were not talking about the same property:
react-native-css-interopships a Babel plugin that rewritesReact.createElementtoReactNativeCSSInterop.createInteropElementin every file except react's and its own — the SDK's files included. Compiling the SDK's publishedlib/with such an app's Babel config shows exactly that:So the SDK saved the host's wrapper and replaced React's own factory with something that calls it. From then on every
createElementcall in the process — React's internals included — went through the host's wrapper, which is input it was never written to receive.The same mismatch applied to the value
stopTrackingrestores from, so uninstalling made it worse rather than better.Why 0.1.3 did not crash
In 0.1.3 both sides were member expressions, so both were rewritten together: the assignment hit a getter-only property and threw, and the factory was never actually replaced. 0.1.4 changed the write to a computed access to remove that exception — and in doing so replaced the factory for the first time, with a mismatched original. The exception it removed was load-bearing.
The fix
Both sides now read through the module object with a computed access, which no plugin of that kind can rewrite. Verified by recompiling the built SDK with such an app's Babel config:
createInteropElementno longer appears anywhere in the output.This branch also keeps a second, independent change: an accessor is left alone rather than redefined, because a host that puts a getter in that slot is managing it. That one was written for the same crash and does not explain it — on this path
React.createElementis a plain writable property — but it is correct on its own terms and is covered by tests.Verification still owed
Repo-side gates pass: 883 tests,
lerna run prepareacross all 9 projects, lint clean, and the compiled-output check above. None of that exercises the crash. Before this ships, a release build withtrackInteractions: trueon an app using nativewind needs to start cleanly and record actions, with the pre-fix build as the negative control.