跳到主要內容

遷移指南

本頁講兩件事:Vue(lh-ui-plus / lh-ui-vue2)→ React 的通用概念映射,以及本庫實作中與 Vue 版有意的差異。

概念映射總表​

Vue(lh-ui-plus)React(@lh-ui/react)
defineProps<T>()interface LhXxxProps + 解構預設參數
withDefaults(...)解構預設值(不用 defaultProps)
emits + emit('change', [val, e])onChange?: (value, event) => void
v-model="x"value={x} onChange={...}
v-model:visible="x"visible={x} onVisibleChange={...}
defineExpose({ ... })forwardRef + useImperativeHandle
<slot>children
<slot name="footer">footer?: ReactNode prop
具名插槽 #headerheader?: ReactNode prop
provide(...) / inject(...)Context.Provider / useContext
provide 必須 computedContext value 必須 useMemo
computed(() => ...)useMemo(() => ..., [deps])
watch(src, cb)useEffect(() => cb(), [src])
watch(src, cb, { immediate: true })useEffect(effect 天生立即執行一次)
onMounted / onUnmounteduseEffect 的 setup / cleanup
<Transition name="lh-x-fade">useTransition(visible, duration) 驅動同名類別名
<Teleport to="body">createPortal(..., document.body)
ref="<name>"useRef<HTMLDivElement>(null)
nextTick()flushSync() 或 effect(通常不需要)
app.use(LhUI)無——命名匯出按需引入
<LhXxx v-bind="obj"><LhXxx {...obj}>

受控與非受控​

Vue 的 modelValue / update:modelValue 在 React 裡拆成兩套 props:

<!-- Vue -->
<LhInput v-model="text" />
// React:受控
<LhInput value={text} onChange={(val) => setText(val)} />

// React:非受控
<LhInput defaultValue="初始值" />

混用(同時傳 value 與 defaultValue)會在開發態打一條告警,與 React 原生表單語義一致。

事件回呼​

所有事件 props 統一 onXxx 命名,回呼簽名統一雙參數 (value, event):

<!-- Vue -->
<LhSelect @change="onChange" />
// React
<LhSelect onChange={(value, option) => console.log(value, option)} />

過渡動畫​

Vue 的 <Transition> 是宣告式的,React 版用自研 useTransition hook 驅動同一套類別名:

import { useTransition } from '@lh-ui/react'

const { mounted, phase } = useTransition(visible, 300)
// 類別名:`lh-dialog-fade-${phase}` —— phase ∈ enter-from / enter-active / enter-to

類別名與 share SCSS 完全一致,視覺表現與 Vue 版逐幀相同。

彈層與傳送​

Vue 的 <Teleport to="body"> 對應 React 的 createPortal,Dialog / Select 下拉 / Tooltip 全部掛到 document.body。定位幾何由 useLhPopper 計算(與 Vue 版共用 @lh-ui/share/core/shared/popper)。

命令式 ref​

Vue 的 defineExpose 對應 forwardRef + useImperativeHandle:

<!-- Vue -->
<LhForm ref="formRef" />
// React
const formRef = useRef<LhFormInstance>(null)
<LhForm ref={formRef} />
<formRef.current?.validate() />

全域設定​

<!-- Vue -->
<LhConfigProvider :size="size">
<App />
</LhConfigProvider>
// React
<LhConfigProvider size={size}>
<App />
</LhConfigProvider>

本庫與 Vue 版的實作差異​

以下差異是 React 形態下的有意調整,遷移時需注意。每條都已在倉庫內登記。

1. 樣式按需注入​

與 lh-ui-plus(全量注入)不同,本庫與 lh-ui-vue2 對齊:包入口不注入樣式,使用者顯式引 theme.css + style/<name>。

2. 構建鏈同 vue2​

vite lib mode + preserveModules + tsc 鏡像直出 d.ts(plus 用 rollup 多入口掃描)。preserveModules 天然支援按需引入,tsx 無 SFC 虛擬模組,tsc 直出 d.ts 最簡。

3. ConfigProvider 消費側兜底​

Vue 版 inject(..., null) 讓每個元件判空。React 版的消費 hook 傳回全欄位 undefined 的兜底物件,元件內不需要判空——這是 React Context 的常見做法,因為 useContext 無法在呼叫處給預設值。

4. notification / loading 的掛載差異​

三者都是命令式服務,但掛載方式不同:message / notification 共用單例容器 (惰性 createRoot + action 頻道),loading 需要掛到任意目標元素(全螢幕或局部), 走 per-instance createRoot,每個實例持有自己的 root 與 overflow / inert 還原控制代碼。

notification 的 API 是物件式(notification.success({ title, content }),對齊 plus 的 Notification(options)),也接受字串簡寫(只渲染標題行)。FLIP 讓位動畫未實作: Vue 的 transition-group 提供的 -move 類別在 React 側無等價原語,佇列增刪時兄弟卡片瞬時跳位。

5. Input 的 onChange 雙參數​

Vue 版的 @input / @change 分開,React 版統一為 onChange(value, event)(另有 onInput 透傳原生輸入事件)。

6. Input autosize 由元件操作 DOM​

Vue 版透過 data-autosize 屬性讓指令在 DOM 層算高度。React 版在 useEffect 裡直接讀 scrollHeight 並寫 textarea.style.height(受 minRows / maxRows 約束),避免引入額外指令機制。

7. Select 定位改用 useLhPopper​

部分 Vue 版本的 Select 是手寫 fixed 定位、不翻轉。React 版統一走 useLhPopper(與 Dialog / Tooltip 同一套幾何、同樣的溢出翻轉與捲動收起)。

8. Dialog 純受控​

plus 的 Dialog 內部持有 isVisible 中間態,用 watch 雙向同步 modelValue。React 版去掉中間態:open 由外部持有,所有關閉路徑只調 onClose。受控語義更純粹,消除 props 與內部狀態的競態。

9. Dialog 生命週期拆成兩組 effect​

Vue 版用 watch(visible, cb, { immediate: true }) 一個回呼處理開關。React 版拆成「open effect」(註冊彈層棧、鎖捲動、觸發 onOpen)與「mounted effect」(focus trap、觸發 onOpened / onClosed),因為過渡動畫的掛載時序與 open 時序在 React 裡是兩個獨立的生命週期。

10. Form 的非同步驗證修正​

plus 的 validateField 用 Promise.resolve(!message),不會等待非同步 validator 傳回的 Promise——非同步驗證恆傳回 true。React 版加了 isThenable 判定,顯式等待非同步 validator 完成後再判定:

if (isThenable(message)) return message.then((error) => !error)

這條缺陷已在家族 bugfix 台賬登記,Vue 版的修正待回填。

11. Tooltip 受控簡化​

plus 的 Tooltip 有 manual 開關配合 visible 受控。React 版直接看 visible prop:傳了 visible 就是受控,onVisibleChange 回呼通知外部,不再保留 manual 中間開關。

12. React 側增強(plus 暫無)​

以下差異是 React 版的增強,遷移時按新能力使用即可:

  • Tag 關閉即卸載:點關閉按鈕後 Tag 立即從 DOM 卸載,隨後才拋 onClose (plus 只拋事件、由外部 v-if 移除節點)。
  • Input 清空回寫:clearable 的清空按鈕會同時拋 onChange(''),受控狀態自動同步 (plus 的 @clear 只拋事件)。
  • Form change 驗證:rule.trigger === 'change' 的規則隨內容區 change 冒泡觸發 (plus 由 reactive 的 watch 驅動)。
  • Form 重設清錯誤:resetFields() 自增內部版本號,各 FormItem 監聽後清空錯誤態 (plus 靠 reactive 鏈路自動清)。
  • Input 密碼顯隱:showPassword prop 在 type="password" 時渲染眼睛按鈕切換明文, plus / vue2 無此 prop。

13. Wave C 元件的 React 特化​

  • Drawer 遮罩預設不可點穿:maskClosable 預設 false(core 的 canDrawerClose 預設拒絕), plus 的 closeOnClickModal 預設 true,遷移時需顯式宣告。焦點圈只在 lockScroll 開啟時 啟用(shouldDrawerTrapFocus),不鎖定捲動時保留背後內容的 Tab 序列。
  • Dropdown 插槽改 prop:plus 的 #dropdown 插槽在 React 側改為同名 dropdown prop; 選單項經 Context 註冊 DOM,roving tabindex 由 activeEl 錨點態驅動 (Vue 版靠 getCurrentInstance 收集子元件實例)。
  • Popover / Popconfirm 受控化:與 Tooltip 一致,傳 visible 即受控、 onVisibleChange 外拋,無 plus 的 manual 開關。

型別遷移速查​

// Vue:props 由 withDefaults 推斷
const props = withDefaults(defineProps<LhButtonProps>(), { type: 'text' })

// React:介面 + 解構預設
function LhButton(props: LhButtonProps) {
const { type = 'text' } = props
}
// Vue:emit 宣告
const emit = defineEmits<{ change: [val: string, e: Event] }>()

// React:props 上的回呼
interface LhXxxProps {
onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void
}