Skip to content

Android: Hybrid Views receive no base ViewProps on React Native >= 0.86 #1656

Description

@ronickg

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:

before and after

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions