Skip to content

Commit 091ac61

Browse files
dennytospmeta-codesync[bot]
authored andcommitted
Fix DimensionValue props crashing ViewManagers on Android (#58039)
Summary: A Fabric component whose spec declares a `DimensionValue` prop: ```js type NativeProps = Readonly<{ ...ViewProps, marginBack?: DimensionValue, }>; ``` crashes the app while React Native is collecting view manager constants, as soon as the `ViewManager` implements that prop with `ReactProp`: ``` java.lang.RuntimeException: Unrecognized type: class com.facebook.yoga.YogaValue for method: MyNativeViewManager#setMarginBack at ViewManagersPropertyCache.createPropSetter(...) at ViewManagerPropertyUpdater$FallbackViewManagerSetter.<init>(...) at ViewManager.getNativeProps(...) at UIManagerModuleConstantsHelper.internal_createConstantsForViewManager(...) ``` ### Root cause Codegen maps `DimensionValue` to the `DimensionPrimitive` reserved type and emits a boxed `Nullable YogaValue` setter on the generated ViewManager interface — this is covered by a committed snapshot: ```java // GeneratePropsJavaInterface-test.js.snap public interface DimensionPropNativeComponentManagerInterface<T extends View> extends ViewManagerWithGeneratedInterface { void setMarginBack(T view, Nullable YogaValue value); } ``` `ViewManagersPropertyCache.createPropSetter` has no branch for `YogaValue`, so it falls through to the `else` and throws. The conversion logic already exists — `DimensionPropConverter` handles exactly the three shapes a `DimensionValue` arrives in (`null`, a `Double` in points, a `String` like `"100%"`) and has its own unit tests. But it is only referenced by generated delegate code (`GeneratePropsJavaDelegate.js`), never by the `ReactProp` path. Compare `ColorPropConverter`, which is wired into *both* paths — that asymmetry is the bug. This adds the missing `DimensionPropSetter`, modelled on `ColorPropSetter`, which delegates wholesale to the existing converter. It reports `mixed` as its prop type because a dimension may be a number or a string, matching how colors are reported. Note the blast radius: `getNativePropSettersForViewManagerClass` builds the setter map for an entire ViewManager class in one pass, so a single unbindable prop takes down every other prop on that manager, and `createConstantsForViewManager` reads `viewManager.nativeProps` unconditionally. That is why this surfaces as a startup crash rather than a failure when the prop is first set. This is the same class of gap as #55350 (`Nullable Float`). With both fixed, every Java type `GeneratePropsJavaInterface` can emit is bindable through `ReactProp`. ## Changelog: [ANDROID] [FIXED] - Fix `RuntimeException: Unrecognized type: class com.facebook.yoga.YogaValue` when a `ViewManager` implements a `DimensionValue` prop with `ReactProp` Pull Request resolved: #58039 Test Plan: Added `testDimensionSetter` and `testFailToUpdateDimensionPropWithArray` to `ReactPropAnnotationSetterTest`, plus a `dimensionProp` on the ViewManager under test. They drive `viewManager.updateProperties(...)` — the same `FallbackViewManagerSetter` -> `getNativePropSettersForViewManagerClass` -> `createPropSetter` path as the crash — and assert the setter receives `YogaValue(10.5f, POINT)` for a number, `YogaValue(100f, PERCENT)` for `"100%"`, and `null` for `null`, and that an unsupported value still surfaces as a `JSApplicationIllegalArgumentException` rather than escaping raw. ``` $ ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest \ --tests "com.facebook.react.uimanager.ReactPropAnnotationSetterTest" BUILD SUCCESSFUL tests=19 failures=0 errors=0 ok testDimensionSetter <- new ok testFailToUpdateDimensionPropWithArray <- new ... (19/19) ``` Reverting only `ViewManagersPropertyCache.kt` and keeping the new tests reproduces the crash: ``` tests=19 failures=19 java.lang.RuntimeException: Unrecognized type: class com.facebook.yoga.YogaValue for method: ReactPropAnnotationSetterTest$ViewManagerUnderTest#setDimensionProp ``` All 19 fail rather than only the two new ones, for the reason described above: the setter map is built for the whole ViewManager class at once. Formatting — `./gradlew ktfmtFormat` leaves both changed files byte-identical. `ViewManagersPropertyCache` is an `internal object` and `DimensionPropSetter` is a `private class`, so `ReactAndroid.api` is unchanged. Reviewed By: christophpurrer Differential Revision: D116911179 Pulled By: javache fbshipit-source-id: c9b91cc3086d01ce7594aefca1e3a673cd723af6
1 parent 8d23b14 commit 091ac61

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewManagersPropertyCache.kt‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import android.content.Context
1111
import android.view.View
1212
import com.facebook.common.logging.FLog
1313
import com.facebook.react.bridge.ColorPropConverter
14+
import com.facebook.react.bridge.DimensionPropConverter
1415
import com.facebook.react.bridge.Dynamic
1516
import com.facebook.react.bridge.DynamicFromObject
1617
import com.facebook.react.bridge.JSApplicationIllegalArgumentException
1718
import com.facebook.react.bridge.ReadableArray
1819
import com.facebook.react.bridge.ReadableMap
1920
import com.facebook.react.uimanager.annotations.ReactProp
2021
import com.facebook.react.uimanager.annotations.ReactPropGroup
22+
import com.facebook.yoga.YogaValue
2123
import java.lang.reflect.Method
2224

2325
/**
@@ -192,6 +194,15 @@ internal object ViewManagersPropertyCache {
192194
}
193195
}
194196

197+
private class DimensionPropSetter(prop: ReactProp, setter: Method) :
198+
PropSetter(prop, "mixed", setter) {
199+
200+
// A DimensionValue arrives from JS as either a number (points) or a string
201+
// (e.g. "100%"), and is nullable, so the conversion is delegated wholesale.
202+
override fun getValueOrDefault(value: Any?, context: Context): Any? =
203+
DimensionPropConverter.getDimension(value)
204+
}
205+
195206
private class BooleanPropSetter(
196207
prop: ReactProp,
197208
setter: Method,
@@ -412,6 +423,7 @@ internal object ViewManagersPropertyCache {
412423
}
413424
ReadableArray::class.java -> ArrayPropSetter(annotation, method)
414425
ReadableMap::class.java -> MapPropSetter(annotation, method)
426+
YogaValue::class.java -> DimensionPropSetter(annotation, method)
415427
else ->
416428
throw RuntimeException(
417429
"Unrecognized type: $propTypeClass for method: ${method.declaringClass.name}#${method.name}",

‎packages/react-native/ReactAndroid/src/test/java/com/facebook/react/uimanager/ReactPropAnnotationSetterTest.kt‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import com.facebook.react.bridge.ReadableMap
1616
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests
1717
import com.facebook.react.uimanager.annotations.ReactProp
1818
import com.facebook.react.uimanager.annotations.ReactPropGroup
19+
import com.facebook.yoga.YogaUnit
20+
import com.facebook.yoga.YogaValue
1921
import org.junit.Before
2022
import org.junit.Test
2123
import org.junit.runner.RunWith
@@ -43,6 +45,8 @@ class ReactPropAnnotationSetterTest {
4345

4446
fun onBoxedIntSetterCalled(value: Int?)
4547

48+
fun onDimensionSetterCalled(value: YogaValue?)
49+
4650
fun onArraySetterCalled(value: ReadableArray?)
4751

4852
fun onMapSetterCalled(value: ReadableMap?)
@@ -135,6 +139,11 @@ class ReactPropAnnotationSetterTest {
135139
viewManagerUpdatesReceiver.onBoxedFloatSetterCalled(value)
136140
}
137141

142+
@ReactProp(name = "dimensionProp")
143+
fun setDimensionProp(v: View?, value: YogaValue?) {
144+
viewManagerUpdatesReceiver.onDimensionSetterCalled(value)
145+
}
146+
138147
@ReactProp(name = "arrayProp")
139148
fun setArrayProp(v: View?, value: ReadableArray?) {
140149
viewManagerUpdatesReceiver.onArraySetterCalled(value)
@@ -337,6 +346,27 @@ class ReactPropAnnotationSetterTest {
337346
Mockito.reset(updatesReceiverMock)
338347
}
339348

349+
@Test
350+
fun testDimensionSetter() {
351+
viewManager.updateProperties(targetView, buildStyles("dimensionProp", 10.5))
352+
Mockito.verify(updatesReceiverMock).onDimensionSetterCalled(YogaValue(10.5f, YogaUnit.POINT))
353+
Mockito.verifyNoMoreInteractions(updatesReceiverMock)
354+
Mockito.reset(updatesReceiverMock)
355+
viewManager.updateProperties(targetView, buildStyles("dimensionProp", "100%"))
356+
Mockito.verify(updatesReceiverMock).onDimensionSetterCalled(YogaValue(100f, YogaUnit.PERCENT))
357+
Mockito.verifyNoMoreInteractions(updatesReceiverMock)
358+
Mockito.reset(updatesReceiverMock)
359+
viewManager.updateProperties(targetView, buildStyles("dimensionProp", null))
360+
Mockito.verify(updatesReceiverMock).onDimensionSetterCalled(null)
361+
Mockito.verifyNoMoreInteractions(updatesReceiverMock)
362+
Mockito.reset(updatesReceiverMock)
363+
}
364+
365+
@Test(expected = JSApplicationIllegalArgumentException::class)
366+
fun testFailToUpdateDimensionPropWithArray() {
367+
viewManager.updateProperties(targetView, buildStyles("dimensionProp", JavaOnlyArray()))
368+
}
369+
340370
@Test
341371
fun testArraySetter() {
342372
val array: ReadableArray = JavaOnlyArray()

0 commit comments

Comments
 (0)