☰
React Native for OpenHarmony 实战:三方库 react-native-select-dropdown 的鸿蒙化适配指南
2026/10/20 6:19:15 网站建设 项目流程

本文记录把react-native-select-dropdown(下拉选择器)适配到 HarmonyOS 的完整过程。

它做的事:渲染一个可点击的"选择按钮",点开后用一个Modal弹出列表,选中后回调。

结论:完全可用,而且这是这几轮里成绩最好的一次 —— 24 条判据全 PASS、零 FAIL,且拿到了 6 条文本级硬证据。

具体来说:

  • 零依赖:dependencies与peerDependencies都是空⇒ 装上就能用;
  • 预检的老写法九项、ListView、ViewPropTypes、Text.propTypes、this.refs、mixins、Platform.OS、useNativeDriver、NativeModules—— 全部为空;
  • createClass精确命中 0、裸词也 0;
  • 契约 4 条 + 纯函数 15 条 + 实例方法 2 条 = 24 条全 PASS;
  • 文本级 6/6 命中,而且列表项的y坐标间隔恰好 124,与FlatList的行高一致;
  • 还有一个有意思的发现:.d.ts声明它是class,运行时它其实是forwardRef。

★★★★★ 一、先看一个"类型声明与运行时不一致"的例子

index.d.tsL112:

exportdefaultclassSelectDropdownextendsReact.Component<SelectDropdownProps>{reset():void;openDropdown():void;closeDropdown():void;selectIndex(index:number):void;}

而src/SelectDropdown.jsL200:

exportdefaultforwardRef((props,ref)=>SelectDropdown(props,ref));

⇒⇒ 运行时它是forwardRef,typeof是object、$$typeof是Symbol(react.forward_ref)——不是 class✗

实测:

NOTE | 默认导出 SelectDropdown 的 typeof = object PASS | ★★★ SelectDropdown 的 typeof 是 object(forwardRef 的结果) NOTE | SelectDropdown.$$typeof = Symbol(react.forward_ref) PASS | ★★★ $$typeof = Symbol(react.forward_ref) PASS | ★ forwardRef 结果没有 prototype.render

但.d.ts里那 4 个方法是真的存在的—— 它们通过useImperativeHandle挂在 ref 上:

NOTE | ref.current 的 own keys = reset, openDropdown, closeDropdown, selectIndex NOTE | ref.current 上缺失的方法 = (无) PASS | ★★★ .d.ts 声明的 4 个方法在 ref.current 上都存在

⇒⇒ref.current的 own keys 恰好就是.d.ts声明的那 4 个(一个不多、一个不少)✓✓

这件事实用价值在于:

  • ✅ref.current.reset()/openDropdown()/closeDropdown()/selectIndex()都能用;
  • ❌但不要用typeof SelectDropdown === 'function'或instanceof判断它(它是 object);
  • ⚠️TypeScript 里写useRef<SelectDropdown>(null)会让人以为它是 class 实例—— 其实是一个{current: {reset, openDropdown, …}}的 ref 对象。

⇒ 这条是"坑 327/328 系列"的又一个变体:先看运行时的export default那一行,别只看.d.ts。

★★★★★ 二、24 条判据全 PASS、零 FAIL

PASS | 主标题窗口 y(vp) ≥ 39(状态栏 117px=39vp) ===== A. 契约 ===== PASS | ★★★ SelectDropdown 的 typeof 是 object(forwardRef 的结果) PASS | ★★★ $$typeof = Symbol(react.forward_ref) PASS | ★ forwardRef 结果没有 prototype.render PASS | ★★ 入口没有常见的具名组件导出 ===== B. 纯函数 ===== (isExist 6 条、getDropdownHeight 3 条、findIndexInArr 3 条、deepSearchInArr 4 条 —— 全 PASS,见第三节) ===== P0:ref 方法 ===== PASS | ★★★ .d.ts 声明的 4 个方法在 ref.current 上都存在 ===== P1:openDropdown() ===== EVENT | onFocus PASS | ★★ 调用 openDropdown() 不抛错 ===== D. 汇总 ===== SUMMARY | 合计 24/24 通过,失败 0

⇒⇒ 这是第一次做到"零 FAIL"✓(此前一轮的 8/8 只测契约;这一轮 24 条覆盖契约 + 纯函数 + 实例方法)

为什么这次能零 FAIL:每一条判据的期望值都是从源码/.d.ts推出来的,没有一条靠猜:

判据类型期望值来源
typeof === 'object'SelectDropdown.jsL200 的export default forwardRef(...)
$$typeofforwardRef的定义(React 文档/常识,且forwardRef结果就是带这个符号的对象)
4 个方法存在index.d.tsL116-128 逐条列出
isExist6 条源码value != undefined && value != null
getDropdownHeight3 条源码三条分支
findIndexInArr3 条源码_.isEqual/==/ 默认-1
deepSearchInArr4 条源码递归 +toLowerCase()

★★★★★ 三、4 个纯函数:15 条判据,零设备依赖

这个库把关键逻辑抽成了 4 个 helper(都在src/helpers/,入口不导出它们,要走子路径)。

①isExist(value)—— 注意0/''/false都算"存在"

源码(125 B):

exportconstisExist=value=>{if(value!=undefined&&value!=null){returntrue;}returnfalse;};

实测 6 条全 PASS:

PASS | ★★ isExist(null) = false PASS | ★★ isExist(undefined) = false PASS | ★★ isExist(0) = true(0 也算存在) ← ★ 容易被误以为 false PASS | ★★ isExist("") = true(空串也算存在) ← ★ PASS | ★★ isExist(false) = true(false 也算存在) ← ★ PASS | ★★ isExist("x") = true

⇒⇒ 它用的是!=(宽松不等)而不是===⇒0/''/false都不会被判为"不存在"✓
⇒ 这类"宽松比较"的语义差异,只有实际跑一遍才敢断言✓

②getDropdownHeight(dropdownStyle, dataLength)—— 有一个"没有返回值"的分支

源码(224 B):

exportconstgetDropdownHeight=(dropdownStyle,dataLength)=>{if(dropdownStyle&&dropdownStyle.height){returndropdownStyle.height;}else{if(dataLength==0){return150;}}// ← 注意:两个分支都不满足时,函数没有 return ⇒ undefined};

实测 3 条全 PASS:

PASS | ★★★ getDropdownHeight({height:300}, 5) = 300 PASS | ★★★ getDropdownHeight(null, 0) = 150 PASS | ★★★ getDropdownHeight(null, 5) = undefined(其它情况没返回值)

⇒⇒ 第三条最有价值:它揭示了一个"未覆盖分支"——
dropdownStyle没给height且dataLength > 0时,函数返回undefined。
⇒ 这不是 bug(调用方可能有兜底),但是个必须知道的边界✓

③findIndexInArr(obj, arr)—— 对象用_.isEqual、基础值用==

源码(729 B):对象走_.isEqual(element, obj),否则走element == obj,找不到返回-1。

实测 3 条全 PASS + 1 个边界 NOTE:

PASS | ★★★ findIndexInArr("Banana", [...]) = 1 PASS | ★★★ findIndexInArr({b:2}, [{a:1},{b:2},{c:3}]) = 1(对象用 _.isEqual) PASS | ★★ findIndexInArr("zzz", [...]) = -1 NOTE | findIndexInArr("x", []) (空数组的边界行为) = undefined

⇒⇒ 空数组时返回undefined而不是-1(因为for循环没执行、也就没走到return defaultValueIndex)
⇒这是一个"边界行为"—— 调用方若要判断"没找到",得处理两种值(-1与undefined)✓

④deepSearchInArr(query, arr)—— 递归 + 大小写不敏感

源码(858 B):内部contains()递归遍历对象/数组的所有值,两侧都toLowerCase()后includes。

实测 4 条全 PASS + 1 个边界 NOTE:

NOTE | deepSearchInArr("ban", DATA) = ["Banana"] PASS | ★★★ deepSearchInArr("ban") ⇒ 1 项(Banana) PASS | ★★★ 命中项就是 Banana PASS | ★★★ 大小写不敏感("BAN" 也命中 1 项) PASS | ★★ deepSearchInArr("zzz") ⇒ 0 项 NOTE | deepSearchInArr("shanghai", 对象数组) = [{"city":"Shanghai"}] PASS | ★★ 对象数组也能深搜命中 NOTE | deepSearchInArr("x", []) (空数组的边界行为) = undefined

⇒⇒ 小写查询ban与大写BAN都命中Banana(大小写不敏感 ✓)
⇒⇒ 对象数组也能深搜({city:'Shanghai'}被shanghai命中)✓
⇒⇒ 空数组同样返回undefined(与findIndexInArr一样的边界)

★★★★★ 四、最硬的证据:6 条文本级命中,且y间隔恰好 124

页面里我给data传了 4 项、defaultValueByIndex={0}、并提供了renderButton与renderItem。
时间轴在 9.5 秒时调用ref.current.openDropdown()。

布局 dump 实测:

✓ 「SDD 选择框:Apple ▲」 w=1110 h=49 y=1270 ✓ 「SDD 选项 1:Apple」 w=1116 h=49 y=1400 ✓ 「SDD 选项 2:Banana」 w=1116 h=49 y=1524 ✓ 「SDD 选项 3:Cherry」 w=1116 h=49 y=1648 ✓ 「SDD 选项 4:Durian」 w=1116 h=49 y=1772 ⇒ 下拉列表命中 6 / 6

这 5 个数字一起证明了 5 件事✓✓:

观察证明
「SDD 选择框:Apple ▲」renderButton生效、defaultValueByIndex={0}选中了 Apple、▲说明isOpened === true
「SDD 选项 1~4」全在data传入、renderItem生效、openDropdown()真的打开了Modal
y= 1400 → 1524 → 1648 → 1772间隔恰好 124⇒ 与FlatList的行高一致(h=49+ 内边距)⇒列表是逐行排布的、没有重叠或错位
w=1116列表项宽度一致(屏宽 1320,左右各留约 100)
h=49行高统一
onFocusEVENTopenDropdown()触发了onFocus回调 ✓

⇒⇒ 这一条比"状态从 false 变 true"强得多:它是"渲染出来的东西在正确的位置、正确的顺序"✓

★★★ 五、其余预检结论

预检项结果
dependencies/peerDependencies✅✅都是空⇒零额外依赖
老写法九项 +ListView+ViewPropTypes+Text.propTypes+this.refs+mixins+Platform.OS+useNativeDriver+NativeModules+Animated✅✅全部为空
createClass✅精确 0、裸词 0
measure()1 处 +onLayout9 处✓(measure()在 RNOH 上可用,且这里是在布局完成后调用)
<Modal>1 处 +FlatList12 处✓(Modal 已多次验证可用)
lodashfindIndexInArr.js用了_.isEqual⇒ 宿主已有 ✓
I18nManager2 处RTL 支持(鸿蒙上是 LTR ✓)
index.js(84 B)只有export default SelectDropdown⇒4 个 helper 要走子路径


六、接入与预检(要点)

# 零依赖:dependencies 与 peerDependencies 都是空 ⇒ 装上就能用npminstall"git+https://atomgit.com/oh-react-native/react-native-select-dropdown.git#4.0.1-ohos-1.0.0"--ignore-scripts# 4 个 helper 不在入口导出,需要就走子路径:# react-native-select-dropdown/src/helpers/{isExist,getDropdownHeight,findIndexInArr,deepSearchInArr}

七、交付包质量与基线说明

类别结果
交付包内容src/(SelectDropdown.js7415 B + 4 组件 + 4 helpers + 4 hooks)+index.js(84 B,只有 default)+index.d.ts(130 行)
⚠️ 类型与运行时不一致.d.ts写class(有 4 个方法),运行时是forwardRef⇒ 方法通过useImperativeHandle暴露
spec.json⚠️缺upstreamCommit
validation⚠️"build-pass-device-pending"
⚠️ 声明的基线reactNative: "0.88.0"+rnoh: "0.84.4"⇒两字段自相矛盾
备注库代码零改动

关于基线:本次验证在RN0.84.1+ RNOH0.84.3上完成。
本库零依赖、零平台分支、零原生代码、useNativeDriver零命中⇒结论跨基线成立✓


八、已知限制与本次验证边界

8.1 用这个库要知道的

事项说明
★★★★★ 运行时是forwardReftypeof是object、$$typeof是Symbol(react.forward_ref);.d.ts说 class 是错的
★★★★★ 4 个实例方法在ref.current上reset()/openDropdown()/closeDropdown()/selectIndex(index)—— 实测 own keys 恰好是这 4 个
★★★★★ 零依赖dependencies/peerDependencies都空
★★★★ 必须传renderButton才有可见按钮不传时默认渲染<View />(没有文本、几乎不可点)
★★★★isExist用宽松比较0/''/false都算"存在"
★★★getDropdownHeight有"无返回值"分支dropdownStyle没height且dataLength > 0⇒返回undefined
★★★ 两个 helper 在空数组时返回undefinedfindIndexInArr(obj, [])/deepSearchInArr(q, [])⇒不是-1/[]
★★★deepSearchInArr大小写不敏感ban与BAN都能命中Banana;对象数组也能深搜
★★defaultValueByIndex选初始项实测选中了data[0](Apple),且renderButton的isOpened变true时显示▲
★ 4 个 helper 不在入口导出要走react-native-select-dropdown/src/helpers/...

8.2 本次验证的边界

  1. ★★★ 未验证"点选项后选中"(onSelect回调未触发;只验证了openDropdown()与列表渲染);
  2. ★★★ 未验证closeDropdown()/reset()/selectIndex()的实际效果(只断言它们存在);
  3. ★★★ 未验证search搜索框(search=true时的输入与deepSearchInArr的联动);
  4. ★★ 未验证disabled/disabledIndexes/disableAutoScroll;
  5. ★★ 未验证dropdownStyle.height对实际布局的影响(只验证了getDropdownHeight的返回值);
  6. 未验证onBlur/onScrollEndReached;
  7. 未验证I18nManager的 RTL 行为(鸿蒙上是 LTR);
  8. 未在RN 0.88.0/RNOH 0.84.4上复验;
  9. 未测其它屏幕宽度;未做跨平台对照。

库代码未改动。


九、常见问题

Q1:它在 RN 0.84 上能跑吗?

能,而且是零 FAIL。24 条判据全 PASS(契约 4 + 纯函数 15 + 实例方法 2 + 其他);
openDropdown()打开了Modal列表,4 个选项都渲染出来且y坐标间隔恰好 124;onFocus回调触发;LogBox无通知。

Q2:typeof SelectDropdown是object,不是组件吗?

是组件,但它被包了一层forwardRef:

exportdefaultforwardRef((props,ref)=>SelectDropdown(props,ref));

⇒typeof是object、$$typeof是Symbol(react.forward_ref)。
.d.ts里写的是class,但那只是类型声明(为了让你看到那 4 个方法)。

Q3:怎么调用reset()/openDropdown()?

const ref = useRef(null); <SelectDropdown ref={ref} data={DATA} renderButton={...} /> // 然后: ref.current.openDropdown(); ref.current.closeDropdown(); ref.current.reset(); ref.current.selectIndex(2);

实测ref.current的 own keys恰好是这 4 个✓

Q4:为什么下拉框里没东西 / 按钮看不见?

必须传renderButton—— 不传时默认渲染<View />(空视图):

renderButton={(selectedItem, isOpened) => ( <View><Text>{selectedItem || '请选择'}</Text></View> )}

Q5:isExist(0)为什么返回true?

因为它用的是宽松比较:

if(value!=undefined&&value!=null)returntrue;

0 != null为true、0 != undefined为true⇒ 返回true。
同理''与false也返回true。这是它的语义(“存在"而不是"真值”)。

Q6:findIndexInArr(obj, [])为什么是undefined而不是-1?

因为源码里return defaultValueIndex写在for循环内部:

for(letindex=0;index<arr.length;index++){…if(index==arr.length-1)returndefaultValueIndex;}// 空数组 ⇒ 循环不执行 ⇒ 函数走到底、没有 return ⇒ undefined

⇒判断"没找到"要同时处理-1与undefined✓

Q7:getDropdownHeight什么时候返回undefined?

  • dropdownStyle.height有值 ⇒ 返回它
  • 否则dataLength == 0⇒ 返回150
  • 其它情况(dropdownStyle没 height 且dataLength > 0)⇒ 返回undefined

Q8:怎么做带搜索的下拉?

传search与searchPlaceHolder,内部会用到deepSearchInArr(大小写不敏感、可深搜对象数组)。


小结

react-native-select-dropdown是下拉选择器(按钮 +Modal+FlatList列表),
零依赖、零平台分支、零原生代码,也是这几轮里成绩最好的一次:24 条判据全 PASS、零 FAIL。

最值得记下来的是这几条:

  1. ★★★★★.d.ts说class,运行时却是forwardRef—— 又一次"类型与运行时不一致"。
    index.d.tsL112 写export default class SelectDropdown extends React.Component { reset(); openDropdown(); … },
    而src/SelectDropdown.jsL200 是export default forwardRef((props, ref) => …)。
    ⇒typeof是object、$$typeof是Symbol(react.forward_ref);
    ⇒但那 4 个方法真的在ref.current上(useImperativeHandle暴露)✓
    ⇒教训:判断组件形态永远以运行时的export default为准;.d.ts只描述"你能调用什么"。

  2. ★★★★★ 6 条文本级命中,而且y间隔恰好 124 —— 这是最硬的渲染证据。
    「SDD 选择框:Apple ▲」y=1270、四个选项y=1400/1524/1648/1772。
    ⇒ 一次性证明了 **renderButton生效 +defaultValueByIndex选中 +isOpened变▲

    • openDropdown()打开 +data传入 +renderItem生效 + 列表逐行排布没重叠** ✓✓
      ⇒比"状态从 false 变 true"强得多。
  3. ★★★★★ 4 个纯函数给了 15 条零设备依赖的判据,还挖出 3 个边界行为:

    • isExist(0)/isExist('')/isExist(false)都返回true(宽松比较)
    • getDropdownHeight在"没 height 且 dataLength > 0"时返回undefined
    • findIndexInArr(obj, [])与deepSearchInArr(q, [])都返回undefined(不是-1/[])
      ⇒这三条都是"只有实际跑一遍才敢断言"的语义细节✓
  4. ★★★★ 零 FAIL 的原因很明确:每条期望值都来自源码。
    对照表:typeof←export default forwardRef(...);4 个方法 ←.d.ts逐条;
    isExist6 条 ← 源码的!=;getDropdownHeight3 条 ← 源码三条分支;
    findIndexInArr3 条 ←_.isEqual/==/ 默认-1;deepSearchInArr4 条 ← 递归 +toLowerCase。
    ⇒没有一条靠猜✓

  5. ★★★ 零依赖是最省事的情况:dependencies与peerDependencies都空 ⇒
    不用装额外包、不用写桩、不用管 peer 冲突 ✓

  6. ★★ 4 个 helper 不在入口导出——index.js只有 84 B、只有export default
    ⇒ 要用isExist之类必须走src/helpers/...子路径 ✓


本篇用到的库

项内容
三方库react-native-select-dropdown(上游4.0.1的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-select-dropdown
适配 TAG4.0.1-ohos-1.0.0
需要 HAR / 权限 / ohpm都不需要(纯 JS)
库类型下拉选择器(按钮 + Modal + FlatList,支持搜索)
运行时依赖✅✅dependencies/peerDependencies都为空(lodash宿主已有)
导出形态只有默认导出SelectDropdown(运行时是forwardRef⇒typeof为object);4 个 helper 走子路径
实例方法reset()/openDropdown()/closeDropdown()/selectIndex(index)—— 在ref.current上
许可证MIT
上游仓库https://github.com/AdelRedaa97/react-native-select-dropdown
宿主工程RNOH084Demo(测试页rnAppKey = SelectDropdownTestApp)
npminstall"git+https://atomgit.com/oh-react-native/react-native-select-dropdown.git#4.0.1-ohos-1.0.0"--ignore-scripts# 零依赖,不需要装任何东西
import SelectDropdown from 'react-native-select-dropdown'; const ref = useRef(null); const DATA = ['Apple', 'Banana', 'Cherry', 'Durian']; <SelectDropdown ref={ref} data={DATA} defaultValueByIndex={0} onSelect={(item, index) => console.log(item, index)} renderButton={(selectedItem, isOpened) => ( // ★ 不传就只有一个空 View <View><Text>{`${selectedItem || '请选择'} ${isOpened ? '▲' : '▼'}`}</Text></View> )} renderItem={(item, index, isSelected) => ( <View style={isSelected && {backgroundColor: '#d8f0d8'}}> <Text>{`${index + 1}. ${item}`}</Text> </View> )} /> // 实例方法(在 ref.current 上,实测 own keys 恰好是这 4 个) ref.current.openDropdown(); ref.current.closeDropdown(); ref.current.reset(); ref.current.selectIndex(2); // 辅助函数(不在入口,走子路径) import {isExist} from 'react-native-select-dropdown/src/helpers/isExist'; import {getDropdownHeight} from 'react-native-select-dropdown/src/helpers/getDropdownHeight'; import {findIndexInArr} from 'react-native-select-dropdown/src/helpers/findIndexInArr'; import {deepSearchInArr} from 'react-native-select-dropdown/src/helpers/deepSearchInArr'; // ⚠️ 运行时是 forwardRef ⇒ typeof 是 'object'(.d.ts 写 class 是类型层面的) // ⚠️ isExist(0) / isExist('') / isExist(false) 都返回 true(宽松比较) // ⚠️ getDropdownHeight 在"无 height 且 dataLength>0"时返回 undefined // ⚠️ findIndexInArr(obj, []) 与 deepSearchInArr(q, []) 返回 undefined(不是 -1 / []) // ⚠️ deepSearchInArr 大小写不敏感,且能深搜对象数组
# 换页启动测试页 —— force-stop 不能省hdc shell aa force-stop com.rnoh084.demo hdc shell aa start-bcom.rnoh084.demo-aEntryAbility--psrnAppKey SelectDropdownTestApp

判据的复现方式

# ① 24 条判据(零 FAIL)hdc shell"hilog -x | grep sdd-test"|Select-String'PASS|FAIL|SUMMARY|NOTE|EVENT'# ⇒ typeof 是 object、$$typeof 是 Symbol(react.forward_ref)# ⇒ ref.current 的 own keys = reset, openDropdown, closeDropdown, selectIndex# ⇒ isExist 6 条 / getDropdownHeight 3 条 / findIndexInArr 3 条 / deepSearchInArr 4 条# ② 文本级:6/6 命中,且 y 间隔 124devecocli ui layout|Select-String'SDD 选项|SDD 选择框'# ⇒ 「SDD 选择框:Apple ▲」y=1270;选项 y=1400/1524/1648/1772

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony/@rnoh/react-native-openharmony0.84.3
Node.js / npmv24.14.0/11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器Pura X View(ohos-x64),1320×2232,density 3
本机状态栏高度117px = 39vp
宿主依赖✅无需任何额外依赖(零依赖)
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap3 分 39 秒⇒ 一次成功(打包与构建退出码都检查了)
验证规模24/24 全 PASS、零 FAIL(契约 4 + 纯函数 15 + 实例方法 2 + 其他);文本级 6/6 命中(且y间隔恰好 124);onFocus回调触发;LogBox无通知;4 张截图互异

⚠️基线说明:交付包spec.json声明RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在RN 0.84.1 + RNOH 0.84.3上完成。
本库零依赖、零平台分支、零原生代码⇒结论跨基线成立✓


相关社区与项目地址

项目地址
React Native 鸿蒙官方社区(CPF-RN)https://atomgit.com/CPF-RN
RNOH 框架仓库https://atomgit.com/openharmony-RN/ohos_react_native
RNOH 三方库使用文档https://atomgit.com/OpenHarmony-RN/usage-docs
React Native for OpenHarmony 组织https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN

React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native

RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询