メインコンテンツへスキップ

移行ガイド

本ページは 2 つのことを説明します: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 は computed でなければならないContext value は useMemo でなければならない
computed(() => ...)useMemo(() => ..., [deps])
watch(src, cb)useEffect(() => cb(), [src])
watch(src, cb, { immediate: true })useEffect(effect は元々直ちに 1 回実行される)
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 では 2 組の props に分かれます:

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

// React:非制御
<LhInput defaultValue="初期値" />

混用(value と defaultValue を同時に渡す)は開発時に警告を 1 つ出します。React ネイティブフォームのセマンティクスと一致します。

イベントのコールバック​

すべてのイベント props は onXxx の命名に統一され、コールバックのシグネチャは 2 引数 (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) で各コンポーネントに null 判定をさせます。React 版の消費 hook は全フィールドが undefined のフォールバックオブジェクトを返し、コンポーネント内で null 判定が不要です —— useContext は呼び出し箇所でデフォルト値を与えられないため、これは React Context でのよくあるやり方です。

4. notification / loading のマウントの差異​

3 つとも命令的サービスですが、マウントの方式が異なります: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 は 2 引数​

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 のライフサイクルを 2 組の effect に分割​

Vue 版は watch(visible, cb, { immediate: true }) の 1 つのコールバックで開閉を処理します。React 版は「open effect」(オーバーレイスタックへの登録、スクロールのロック、onOpen の発火)と「mounted effect」(focus trap、onOpened / onClosed の発火)に分割します。トランジションのマウントのタイミングと open のタイミングは React では 2 つの独立したライフサイクルだからです。

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
}