🫥
Vue3系をReactにBridgeしたいと思ったそこのあなたへ
はじめに
Vue3系をReactでBridgeする必要はあるのか?と思った、そこのあなた。大正解です。
筆者自身もニーズが不確かなまま執筆しています。ぜひ、箸休め程度に見て頂けると嬉しいです。
イメージ
GifのようにReact(Next.js)上で、Vue3系のFormComponentを呼び出しています。
リポジトリ
Vue
Next.jsReact(Next.js)でVueComponentをBridgeする
Vue componentの定義
先ほどのGifにあったFormComponentは以下のように定義しています。
特筆するようなことはありませんが、検証の為に分かりやすく姓・名に対応する更新用のcallbackを渡しています。
詳細は割愛させて頂きますが、onamae-formという名前でnpm packageとしてライブラリ化を行います。
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{
firstName?: string
lastName?: string
onChangeFirstName?: (value: string) => void
onChangeLastName?: (value: string) => void
}>()
const colObject = {
display: 'grid',
gap: '16px'
}
const firstNameInput = ref(props.firstName || '')
const changeFirstName = (event: Event) => {
if (props.onChangeFirstName) {
const value = (event.target as HTMLInputElement).value
props.onChangeFirstName(value)
firstNameInput.value = value
}
}
const lastNameInput = ref(props.lastName || '')
const changeLastName = (event: Event) => {
if (props.onChangeLastName) {
const value = (event.target as HTMLInputElement).value
props.onChangeLastName(value)
lastNameInput.value = value
}
}
</script>
<template>
<div>
<h1>お名前フォーム</h1>
<div :style="colObject">
<input v-model="firstNameInput" @input="changeFirstName" />
<input v-model="lastNameInput" @input="changeLastName" />
</div>
</div>
</template>
React(Next.js)の定義
全文
----
import { OnamaeForm } from 'onamae-form'
----
export default function Home() {
const [onamae, setOnamae] = useState<{ firstName: string; lastName: string }>({
firstName: '',
lastName: ''
})
const onChangeFirstName = useCallback((value: string) => {
setOnamae((prev) => ({ ...prev, firstName: value }))
}, [])
const onChangeLastName = useCallback((value: string) => {
setOnamae((prev) => ({ ...prev, lastName: value }))
}, [])
const onamaeFormVuePropsRef = useRef({
firstName: onamae.firstName,
lastName: onamae.lastName,
onChangeFirstName,
onChangeLastName
})
useEffect(() => {
let onamaeFormVue: App<Element> | null = null
if (!onamaeFormVue && rootForVue.current) {
onamaeFormVue = createApp({
setup() {
return () => {
const _h = h(OnamaeForm)
_h.props = {
...onamaeFormVuePropsRef.current
}
return _h
}
}
})
onamaeFormVue.mount("#onamae-form")
}
return () => {
if (onamaeFormVue) {
onamaeFormVue.unmount()
}
}
}, [])
return (
<main className={`flex gap-y-6 flex-col items-center justify-between p-24 ${inter.className}`}>
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
<div>
<p>姓:{onamae.firstName}</p>
<p>名:{onamae.lastName}</p>
</div>
<div id="onamae-form" />
</main>
)
}
- VueComponentへ依存させるstate,callbackを定義
----
const onChangeFirstName = useCallback((value: string) => {
setOnamae((prev) => ({ ...prev, firstName: value }))
}, [])
const onChangeLastName = useCallback((value: string) => {
setOnamae((prev) => ({ ...prev, lastName: value }))
}, [])
----
- VueComponent用のpropsを定義
3. ライフサイクルに乗せる必要がないのでuseRefを使用
4. リアクティブに同期させる場合はこの限りではありません
---
const onamaeFormVuePropsRef = useRef({
firstName: onamae.firstName,
lastName: onamae.lastName,
onChangeFirstName,
onChangeLastName
})
---
- createAppでVueAppをインスタンス化
- hyperscriptを用いて、ライブラリ化したVueComponentをVNodeに変換
---
onamaeFormVue = createApp({
setup() {
return () => {
const _h = h(OnamaeForm)
_h.props = {
...onamaeFormVuePropsRef.current
}
return _h
}
}
})
---
- VueComponentをDOMへマウント
----
onamaeFormVue.mount("#onamae-form")
----
- クリーンアップ時にVueComponentをアンマウント
return () => {
if (onamaeFormVue) {
onamaeFormVue.unmount()
}
}
実行してみる
今回はNext.jsのルートファイルで直接呼び出しているので、yarn run dev
などで実行すると冒頭のイメージのように実行されます。
全文
import Image from 'next/image'
import { Inter } from 'next/font/google'
import { OnamaeForm } from 'onamae-form'
import { useCallback, useEffect, useRef, useState } from 'react'
import { App, createApp, h } from 'vue'
const inter = Inter({ subsets: ['latin'] })
export default function Home() {
const [onamae, setOnamae] = useState<{ firstName: string; lastName: string }>({
firstName: '',
lastName: ''
})
const onChangeFirstName = useCallback((value: string) => {
setOnamae((prev) => ({ ...prev, firstName: value }))
}, [])
const onChangeLastName = useCallback((value: string) => {
setOnamae((prev) => ({ ...prev, lastName: value }))
}, [])
const onamaeFormVuePropsRef = useRef({
firstName: onamae.firstName,
lastName: onamae.lastName,
onChangeFirstName,
onChangeLastName
})
useEffect(() => {
let onamaeFormVue: App<Element> | null = null
if (!onamaeFormVue) {
onamaeFormVue = createApp({
setup() {
return () => {
const _h = h(OnamaeForm)
_h.props = {
...onamaeFormVuePropsRef.current
}
return _h
}
}
})
onamaeFormVue.mount('#onamae-form')
}
return () => {
if (onamaeFormVue) {
onamaeFormVue.unmount()
}
}
}, [])
return (
<main className={`flex gap-y-6 flex-col items-center justify-between p-24 ${inter.className}`}>
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
<div>
<p>姓:{onamae.firstName}</p>
<p>名:{onamae.lastName}</p>
</div>
<div id="onamae-form" />
</main>
)
}
おわりに
いかがでしたでしょうか。思ったよりも簡単に呼び出せるのが印象的でした。
ですが、プロダクションレベルでは思想が違うライブラリを共存させるのは中々辛さそうだなと思います。
次はVNodeをReactNodeへ変換するParserでも作ってみようかと思っています。
Discussion