🫥

Vue3系をReactにBridgeしたいと思ったそこのあなたへ

2024/07/10に公開

はじめに

Vue3系をReactでBridgeする必要はあるのか?と思った、そこのあなた。大正解です。
筆者自身もニーズが不確かなまま執筆しています。ぜひ、箸休め程度に見て頂けると嬉しいです。

イメージ

GifのようにReact(Next.js)上で、Vue3系のFormComponentを呼び出しています。

リポジトリ

Vue
https://github.com/yuto-ohshima/onamae-form-vue
Next.js
https://github.com/yuto-ohshima/bredge-onamae-form-vue

React(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>
  )
}
  1. VueComponentへ依存させるstate,callbackを定義
----
  const onChangeFirstName = useCallback((value: string) => {
    setOnamae((prev) => ({ ...prev, firstName: value }))
  }, [])
  const onChangeLastName = useCallback((value: string) => {
    setOnamae((prev) => ({ ...prev, lastName: value }))
  }, [])
----
  1. VueComponent用のpropsを定義
    3. ライフサイクルに乗せる必要がないのでuseRefを使用
    4. リアクティブに同期させる場合はこの限りではありません
---
  const onamaeFormVuePropsRef = useRef({
    firstName: onamae.firstName,
    lastName: onamae.lastName,
    onChangeFirstName,
    onChangeLastName
  })
---
  1. createAppでVueAppをインスタンス化
  2. hyperscriptを用いて、ライブラリ化したVueComponentをVNodeに変換
---
  onamaeFormVue = createApp({
    setup() {
      return () => {
        const _h = h(OnamaeForm)
          _h.props = {
            ...onamaeFormVuePropsRef.current
          }

        return _h
      }
    }
  })
---
  1. VueComponentをDOMへマウント
----
  onamaeFormVue.mount("#onamae-form")
----
  1. クリーンアップ時に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でも作ってみようかと思っています。

株式会社CHILLNN

Discussion