Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
5 tasks
React Native 0.87 removed the initializeDynamicProps call from the Props constructor (Props::Props -> Props::initialize), leaving ConcreteComponentDescriptor::cloneProps as its only caller. ViewComponentDescriptor overrides cloneProps to use Nitro's cached copy constructor and did not make that call. Up to RN 0.85 that was harmless because the constructor did it; from RN 0.86 onwards Props::rawProps stays empty, and on Android that map is what gets serialized to Java and handed to ViewManager.updateProperties. The result is that a Hybrid View receives no base ViewProps at all on Android - no backgroundColor, no transform, no opacity, no testID - while iOS is unaffected because RCTViewComponentView reads the typed C++ fields directly. Nothing warns: React Native never sees the props, so it never reports an unsupported one. Measured on RN 0.87.1: Props::rawProps held 0 entries before this call and 102 after. On RN 0.85.3 it already held the view's props, which is why the example app - pinned to 0.85.3 - does not show the symptom. Adds a harness test asserting opacity reaches the native view. TestView paints its own surface so backgroundColor is masked, but nothing native can override a view's alpha. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The base-view-prop tests could not fail on the pinned RN 0.85.3: `Props::Props` still filled `Props::rawProps` there, so the missing `initializeDynamicProps` call in `ViewComponentDescriptor::cloneProps` had no visible effect. Turning on `enableExclusivePropsUpdateAndroid` makes 0.85 take the same path 0.86+ takes unconditionally, which is what the tests are meant to cover. It can be dropped once the floor is >= 0.86. Also fixes the opacity assertion: it asserted on PNG alpha, which is always 255 in a screen capture. It now asserts the blue surface washes out at 10% alpha. Measured on an API 36 emulator, full `nitro.views.harness`: without the cloneProps fix: 8 failed, 2 passed with it: 10 passed The 11 ArrayBuffer/HardwareBuffer failures in `nitro.harness` are pre-existing on this emulator and identical with and without the flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ronickg
force-pushed
the
repro/android-view-props
branch
from
September 21, 2026 20:53
682b537 to
75f9cad
Compare
ronickg
added a commit
to ronickg/react-native-nitro-rolling-number
that referenced
this pull request
Sep 21, 2026
…ver both packages (#5) * Android: Hybrid Views were getting none of their base view props From React Native 0.86 a Nitro Hybrid View on Android received no backgroundColor, no border*, no transform, no opacity, no testID and no accessibility props, while iOS was fine. There is no error to go with it: React Native never sees the prop, so it never warns. On Android those props only reach the view through Props::rawProps, a folly::dynamic that is serialised to Java and handed to ViewManager.updateProperties. The one thing that fills that map is initializeDynamicProps, which React Native calls at the end of ConcreteComponentDescriptor::cloneProps. Nitro overrides cloneProps to use its cached copy constructor and, as shipped in 0.37.1, never makes that call. The map stays empty and every style prop disappears. The patch adds the call back. Filed upstream as margelo/nitro#1656 with the fix in margelo/nitro#1655; drop the patch, the patchedDependencies entry and the tripwire test once a release contains it. The tripwire (NitroViewProps.test.tsx) reads the installed Nitro source and fails if the patch stops being applied - a version bump, a fresh install without patches - and separately if React Native moves the call, in which case the patch needs rewriting rather than reapplying. The example gets a two-box repro screen so the symptom can be seen on a device rather than inferred. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The onboarding flow reads as an onboarding, not as a test harness The four keyboard screens exist to compare NitroInput with TextInput through a realistic flow, and they looked like the harness they are: "Step 2 / 4" in grey, every field inside a captioned card, the keyboard timeline taking half the screen, a navigation title competing with the question. Now each step is a progress rail, the question as a headline with one line under it, the field, and one button. The timeline is still there - it is the reason the screens exist - but folded away behind "Show timeline", so the flow reads as the thing it imitates. The three pushed steps share their chrome through one options object: no title, a minimal back chevron, no header hairline. The search sheet hides its header, since the sheet's grabber is the chrome, and its Done button names what was picked. The details form focuses its first field on mount, like the email step already did. Every testID is unchanged, so the recorded flows still run. The native reference apps under native/ (UIKit, SwiftUI, Android views and Compose, the same four screens for comparing keyboard behaviour) and the screen recordings under example/.recordings/ are ignored: reference material, like the *-INTERNALS.md notes, not part of the library. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * The root scripts only knew about one of the two packages Every root script routed to packages/react-native-nitro-rolling-number, so `bun run test` at the root never ran the input package's suite, and plain `bun test` at the root ran bun's own runner against jest files and reported failures that were not there. Both packages are live - the example imports both - so the scripts now fan out over packages/* with bun's --filter, and test:cpp joins them since both packages have one. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Ronald Goedeke <ronald@margelo.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This branch was previously deployed
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 #1656.
The problem
ViewComponentDescriptor::clonePropsoverrides React Native's implementation anddoes not call
initializeDynamicProps. Up to RN 0.85 that was harmless, becauseProps::Propscalled it for you viaProps::initialize. RN 0.86 removed that, sonothing fills
Props::rawPropsfor a Hybrid View any more — and on Android thatmap is exactly what gets serialized to Java and applied by
ViewManager.updateProperties.The effect: a Hybrid View silently receives no
backgroundColor,border*,transform,opacity,testIDor accessibility props on Android. iOS isunaffected, because
RCTViewComponentViewreads the typed C++ fields directly.Nothing warns, because React Native never sees a prop to report as unsupported.
Left is a plain RN
View, right isTestView, same style object, on RN 0.87.1with no feature flags:
Props::rawPropshas exactly one writer per RN version:Props::rawPropsProps::initialize(from the constructor) +ConcreteComponentDescriptor::clonePropsConcreteComponentDescriptor::clonePropsonly(
ShadowNode::propsForClonedShadowNodealso touches it, but it is gated on!rawProps.empty()— it merges, it cannot create.)A corroborating detail: nitrogen generates a per-view
filterObjectKeysand threadsit through
ViewProps → BaseViewProps → YogaStylableProps → Props, where ReactNative's only consumer of it is
rawProps.toDynamic(filterObjectKeys)insideinitializeDynamicProps. Without that call it is dead code.The fix
This does add a
rawProps.toDynamic()per props clone, which may be part of whatthe override was avoiding. If you'd prefer something narrower — serializing only
the base
ViewPropskeys — I'm happy to rework it that way.Why the harness didn't catch it
It couldn't. On the pinned RN 0.85.3 the constructor still fills the map, so the
bug has no observable effect. Turning on
enableExclusivePropsUpdateAndroidmakes0.85 take the same path 0.86+ takes unconditionally, and then it does — and not
subtly.
queryByTestIdresolves against the native view tree, andtestIDisitself a base view prop, so most of the file can't even find the view it rendered:
Six of those eight are existing tests, not ones added here:
What's in this PR
cpp/views/ViewComponentDescriptor.hppapps/example/android/.../MainApplication.ktenableExclusivePropsUpdateAndroidso the harness exercises the 0.86+ path. Droppable once the floor is >= 0.86, where it's unconditional.apps/example/__tests__/nitro.views.harness.tsxapplies opacity to the native view, plus a control)The opacity test asserts the blue surface washes out at 10% alpha rather than
asserting on PNG alpha — a screen capture is always alpha 255, so that would
have passed either way.
Verification
bun specs— no codegen driftbun typecheck— cleanbun example lint-ci— cleanclang-format -style=file:./config/.clang-formaton the changed header — cleanbun lint-kotlin— ktlint not installed locally; its scope ispackages/*/android, and the Kotlin change here is underapps/examplenitro.harness(the other file) shows 11 pre-existing ArrayBuffer/HardwareBufferfailures on this emulator — identical with and without the flag, verified with a
control build
A clean, fix-free reproduction on RN 0.87.1 is on
bump/rn-0.87: build it andopen the example's View tab — that's the screenshot above.
Tested on a Pixel 9 API 36 emulator (Android 16, arm64). I have not run this on
physical hardware or on iOS; the iOS claim above is from reading
RCTViewComponentView, not from a measurement.🤖 Generated with Claude Code