What happened?
ViewComponentDescriptor::cloneProps overrides React Native's implementation and
does not call initializeDynamicProps. Up to RN 0.85 that was harmless, because
Props::Props called it for you via Props::initialize. RN 0.86 removed that, so
nothing fills Props::rawProps for a Hybrid View any more — and on Android that
map is exactly what gets serialized to Java and applied by
ViewManager.updateProperties.
The effect: a Hybrid View silently receives no backgroundColor, border*,
transform, opacity, testID or accessibility props on Android. iOS is
unaffected, because RCTViewComponentView reads 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 is TestView, same style object, on RN 0.87.1
with no feature flags and an otherwise unmodified example app:

opacity is the prop to watch: TestView paints its own surface, which masks
whether backgroundColor arrived, but nothing native can override a view's alpha.
Why it happens. Props::rawProps has exactly one writer per RN version:
|
writers of Props::rawProps |
| RN 0.85.3 |
Props::initialize (from the constructor) + ConcreteComponentDescriptor::cloneProps |
| RN 0.87.1 |
ConcreteComponentDescriptor::cloneProps only |
(ShadowNode::propsForClonedShadowNode also touches it, but it is gated on
!rawProps.empty() — it merges, it cannot create.)
Android then does, in SurfaceMountingManager.kt:
viewState.currentProps = ReactStylesDiffMap(props)
viewManager.updateProperties(view, viewState.currentProps)
An empty rawProps means an empty ReactStylesDiffMap, so every @ReactProp
setter on BaseViewManager is skipped.
One corroborating detail: nitrogen generates a per-view filterObjectKeys and
threads it through ViewProps → BaseViewProps → YogaStylableProps → Props, where
React Native's only consumer of it is rawProps.toDynamic(filterObjectKeys) inside
initializeDynamicProps. Without that call it is dead code.
Suggested fix (this is what #1655 does):
auto newProps = TShadowNode::Props(context, /* & */ rawProps, props);
#ifdef RN_SERIALIZABLE_STATE
TShadowNode::initializeDynamicProps(newProps, rawProps, props);
#endif
return newProps;
It does add a rawProps.toDynamic() per props clone, which may be part of what the
override was avoiding — if you'd prefer something narrower (serializing only base
ViewProps keys), happy to rework it.
Why the harness never caught this. It couldn't: on the pinned 0.85.3 the
constructor still fills the map, so the bug has no observable effect. With
enableExclusivePropsUpdateAndroid on — the path 0.86+ takes unconditionally — it
does, and not subtly. queryByTestId resolves against the native view tree and
testID is itself a base view prop, so most of the file can't find the view it
just rendered:
full nitro.views.harness, Pixel 9 API 36 emulator, flag on
without the fix .... 8 failed, 2 passed
with it ............ 10 passed
Six of those eight are existing tests.
Reproductions, both off b06f548 and buildable as-is:
| branch |
what it is |
bump/rn-0.87 |
RN 0.87.1 + the two boxes above on the example's View tab. No fix, no flags. Build it, open the View tab — that is the screenshot. |
| #1655 |
the fix, the flag that lets the harness exercise it, and tests |
Reproduceable Code
const box = {
width: 120,
height: 120,
backgroundColor: '#00C000',
transform: [{ rotate: '10deg' }],
opacity: 0.4,
} as const
// Same style object on both. On Android the Nitro view ignores all of it.
Relevant log output
ViewComponentDescriptor::cloneProps instrumented to print newProps->rawProps.size()
example on RN 0.85.3, flag off (today's default) ........ 5
example on RN 0.85.3, enableExclusivePropsUpdateAndroid . 0 (13,405 consecutive clones)
example on RN 0.87.1, unpatched ......................... 0 (6,508 consecutive clones)
our own app on RN 0.87.1, patched ....................... 102
Nothing is logged by React Native in the broken case - an empty rawProps means
an empty ReactStylesDiffMap, so there is no unsupported prop for it to warn about.
Device
Pixel 9 API 36 emulator (Android 16, arm64). Not tested on physical hardware; the iOS claim above is from reading RCTViewComponentView, not a measurement.
Nitro Modules Version
0.37.1, and main (b06f548)
Nitrogen Version
0.37.1
Can you reproduce this issue in the Nitro Example app here?
Yes, I can reproduce the same issue in the Example app here
Additional information
What happened?
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 and an otherwise unmodified example app:
opacityis the prop to watch:TestViewpaints its own surface, which maskswhether
backgroundColorarrived, but nothing native can override a view's alpha.Why it happens.
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.)Android then does, in
SurfaceMountingManager.kt:An empty
rawPropsmeans an emptyReactStylesDiffMap, so every@ReactPropsetter on
BaseViewManageris skipped.One corroborating detail: nitrogen generates a per-view
filterObjectKeysandthreads it through
ViewProps → BaseViewProps → YogaStylableProps → Props, whereReact Native's only consumer of it is
rawProps.toDynamic(filterObjectKeys)insideinitializeDynamicProps. Without that call it is dead code.Suggested fix (this is what #1655 does):
It does add a
rawProps.toDynamic()per props clone, which may be part of what theoverride was avoiding — if you'd prefer something narrower (serializing only base
ViewPropskeys), happy to rework it.Why the harness never caught this. It couldn't: on the pinned 0.85.3 the
constructor still fills the map, so the bug has no observable effect. With
enableExclusivePropsUpdateAndroidon — the path 0.86+ takes unconditionally — itdoes, and not subtly.
queryByTestIdresolves against the native view tree andtestIDis itself a base view prop, so most of the file can't find the view itjust rendered:
Six of those eight are existing tests.
Reproductions, both off
b06f548and buildable as-is:bump/rn-0.87Reproduceable Code
const box = {
width: 120,
height: 120,
backgroundColor: '#00C000',
transform: [{ rotate: '10deg' }],
opacity: 0.4,
} as const
// Same style object on both. On Android the Nitro view ignores all of it.
Relevant log output
ViewComponentDescriptor::cloneProps instrumented to print newProps->rawProps.size()
example on RN 0.85.3, flag off (today's default) ........ 5
example on RN 0.85.3, enableExclusivePropsUpdateAndroid . 0 (13,405 consecutive clones)
example on RN 0.87.1, unpatched ......................... 0 (6,508 consecutive clones)
our own app on RN 0.87.1, patched ....................... 102
Nothing is logged by React Native in the broken case - an empty rawProps means
an empty ReactStylesDiffMap, so there is no unsupported prop for it to warn about.
Device
Pixel 9 API 36 emulator (Android 16, arm64). Not tested on physical hardware; the iOS claim above is from reading
RCTViewComponentView, not a measurement.Nitro Modules Version
0.37.1, and
main(b06f548)Nitrogen Version
0.37.1
Can you reproduce this issue in the Nitro Example app here?
Yes, I can reproduce the same issue in the Example app here
Additional information