GW の技術チャレンジ
大型連休は何か一つ技術的に新しいことに取り組むことにしています。2025年の GW は React に取り組みました。
私は今まで Vue / Nuxt がメインで、あまり React を触って来ませんでした。大型連休はある程度まとまった時間も取れることだし、集中して触れるだろうと思ったので React に取り組みました。
まずゴールを決めました。
- チュートリアルを一通り終える
- Vue / Nuxt プロジェクトの中で React を使う方法を確立する
チュートリアルは本も検討しましたが、公式サイトの三目並べを作るチュートリアルが分かりやすそうだったのでそれに取り組むことにしました。
React を Vue の中で使う
もう一つのゴールである、「Vue / Nuxt プロジェクトの中で React を使う」ですが、私個人のフロントエンドのコードベースはほとんど Nuxt です。
このままだと連休が終わった後に React のコードを書く機会が作りづらいです。私はこれを機に React を本格的に勉強しようと思っています。
既存の Nuxt プロジェクトの中に React を組み込むことで、日常的に React のコードを書く場所を作る狙いがありました。
この記事の趣旨
前置きが長くなってしまいましたがこの記事では、「Vue / Nuxt プロジェクトの中で React を使う方法」を紹介します。
完成したコード
以下、完成したコードです。
ReactInVue.vue
<template>
<div ref="mountPoint" />
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watchEffect, useAttrs } from 'vue'
import { createElement, type ComponentType } from 'react'
import { createRoot, type Root } from 'react-dom/client'
// Props を受け取る
const props = defineProps<{
component: ComponentType<Record<string, unknown>>
}>()
// Vue の DOM 参照と属性
const mountPoint = ref<HTMLElement | null>(null)
const attrs = useAttrs()
let reactRoot: Root | null = null
// React をレンダリングする関数
function renderReactComponent() {
if (reactRoot && props.component) {
reactRoot.render(createElement(props.component, attrs))
}
}
onMounted(() => {
if (mountPoint.value) {
reactRoot = createRoot(mountPoint.value)
renderReactComponent()
}
})
onBeforeUnmount(() => {
reactRoot?.unmount()
})
// Vue 側からの props 変更を検知して React 側を再描画
watchEffect(() => {
renderReactComponent()
})
</script>
概要
- Vue 3 の Composition API を使って書かれたコンポーネントです。
- Vue から渡された React コンポーネントを、Vue のテンプレート内の
div
に React でレンダリングします。 react-dom/client
のcreateRoot
を使って React のレンダリングを行います。
解説
テンプレート
<template>
<div ref="mountPoint" />
</template>
- この
div
要素は React コンポーネントをマウントする「場所」です。 ref="mountPoint"
によって、Vue からこの DOM 要素を参照できるようにしています。
インポート部分
import { ref, onMounted, onBeforeUnmount, watchEffect, useAttrs } from 'vue'
import { createElement, type ComponentType } from 'react'
import { createRoot, type Root } from 'react-dom/client'
ref
,onMounted
,onBeforeUnmount
,watchEffect
: Vueのライフサイクルなどを使うためのAPI。useAttrs
: Vue の親から渡された属性(propsで定義されていないものも含む)を取得。createElement
: React の要素を生成。createRoot
: React 18 以降の新しいレンダリング API。ComponentType<Record<string, unknown>>
: React コンポーネントの型指定。
Props の受け取り
const props = defineProps<{
component: ComponentType<Record<string, unknown>>
}>()
- このコンポーネントは、
component
という名前の React コンポーネントをprops
として受け取ります。 Record<string, unknown>
は props の型を柔軟に受け取るための汎用的な書き方です。
DOM 参照と属性取得
const mountPoint = ref<HTMLElement | null>(null)
const attrs = useAttrs()
let reactRoot: Root | null = null
mountPoint
: React コンポーネントをマウントするターゲットの DOM。attrs
: Vue 側から渡された props やイベントハンドラなどを取得。reactRoot
:createRoot()
で作成される React のルートオブジェクト。
React をレンダリングする関数
function renderReactComponent() {
if (reactRoot && props.component) {
reactRoot.render(createElement(props.component, attrs))
}
}
createElement
を使って React のコンポーネントを作成。attrs
をそのまま props として渡すことで、Vue 側からのデータやイベントを React に引き継げます。
マウント時の処理
onMounted(() => {
if (mountPoint.value) {
reactRoot = createRoot(mountPoint.value)
renderReactComponent()
}
})
- Vue コンポーネントがマウントされたタイミングで、React コンポーネントも
mountPoint
にマウントされます。
アンマウント時のクリーンアップ
onBeforeUnmount(() => {
reactRoot?.unmount()
})
- Vue コンポーネントが破棄されるときに、React 側もアンマウントしてメモリリークを防ぎます。
props 変更の監視と再描画
watchEffect(() => {
renderReactComponent()
})
- Vue の
props
やattrs
が変更されたら React コンポーネントを再描画します。 watchEffect
は依存関係を自動で追跡するため、変更があればrenderReactComponent()
が自動で呼ばれます。
使い方の例
Vue 側でこのように使います:
<ReactInVue :component="MyReactComponent" someProp="value" />
すると、someProp
などは attrs
経由で React に渡されて、MyReactComponent
にレンダリングされます。
さいごに
イベント処理など、対応できてない部分もありますが、とりあえずレンダリングすることはできました。これから少しずつ改良していこうと思います。
ちなみに React に取り組んでみて、もっと早くやればよかったと思いました。Vue と対比しながら理解していっているので、すんなり理解できています。コンポーネントを JS / TS の関数としてまとめられるところが気に入っています。