Vue.jsからReactへの移行を考えていく中で、Vue.jsのComposition APIにおける機能やパターンをReactでどのように再現できるか疑問に思うことが多かったのでまとめていきたいと思います。
Contents
Vue.jsのComposition APIメソッド
Composition APIはVue 3から導入された新しいAPIであり、コンポーネントのロジックをより明瞭に、かつ再利用可能に書くための方法を提供しています。ここでは、メソッドの書き換えについて考えます。
<template>
<button @click="incrementCount">Clicked {{ count }} times</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref<number>(0)
function incrementCount() {
count.value += 1
}
</script>
上記の例では、incrementCount
というメソッドを直接setup
関数内に定義しています。この関数はテンプレート内でクリックイベントのハンドラとして使われています。
Reactでの同等の実装
Reactでは、関数コンポーネントの中で関数を定義することができます。この関数は、JSX内や他の関数の中で直接使うことが可能です。
import React, { useState } from 'react'
const CounterButton: React.FC = () => {
const [count, setCount] = useState<number>(0)
function incrementCount() {
setCount(prevCount => prevCount + 1)
}
return <button onClick={incrementCount}>Clicked {count} times</button>
}
emitのマッピング
Vue.jsでは、子コンポーネントから親コンポーネントに情報を伝えるためにemit
を使用します。これはカスタムイベントを発火させることで親に情報を伝える仕組みです。
<!-- 子コンポーネント -->
<template>
<button @click="handleClick">Send Message to Parent</button>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
const emit = defineEmits(["customEvent"])
function handleClick() {
emit('customEvent', 'Hello from child!')
}
</script>
<!-- 親コンポーネント -->
<template>
<ChildComponent @customEvent="handleCustomEvent" />
</template>
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'
function handleCustomEvent(message: string) {
console.log(message) // Outputs: "Hello from child!"
}
</script>
Reactでの同等の実装
Reactでは、コンポーネントの階層構造を通じて情報を伝播させるために、関数をpropsとして渡すアプローチを採用しています。子コンポーネントは、親から受け取った関数を実行することで、自身の状態やデータを親に伝えることができます。
// 子コンポーネント
type ChildProps = {
onButtonClick: (message: string) => void;
}
const ChildComponent: React.FC<ChildProps> = ({ onButtonClick }) => {
function handleClick() {
onButtonClick('Hello from child!')
}
return <button onClick={handleClick}>Send Message to Parent</button>
}
// 親コンポーネント
const ParentComponent: React.FC = () => {
function handleChildMessage(message: string) {
console.log(message) // Outputs: "Hello from child!"
}
return <ChildComponent onButtonClick={handleChildMessage} />
}
computed のマッピング
Vue.jsではcomputed
を用いることで、リアクティブなデータに基づく計算プロパティを定義できます。computed
は他のリアクティブなプロパティの変更をトリガーとして再評価されるプロパティを定義するためのものです。これはパフォーマンスの最適化のためのものであり、依存関係が変更されない限り、再計算は行われません。このように、不要な再計算を避けることができます。
<script setup lang="ts">
import { ref, computed } from 'vue'
const count = ref<number>(0)
const doubled = computed(() => count.value * 2)
</script>
Reactでの同等の実装
Reactでは、useState
を使用してコンポーネントの状態を管理します。この状態はリアクティブではないので、手動で更新する必要があります。
一方、useMemo
はメモ化された値を返すフックでこれは、指定された依存関係リスト内の変数が変更されたときのみ再計算されます。これにより、不要な再計算を避けることができます。
したがって、useState
とuseMemo
を組み合わせることで、Vue.jsのcomputed
と似たような振る舞いを持つリアクティブな値をReactで作成することができます。useState
によって状態をトラックし、その状態に基づいて何らかの計算を行う場合には、その計算をuseMemo
でラップしてパフォーマンスを最適化します。
import React, { useState, useMemo } from 'react'
const CounterDisplay: React.FC = () => {
const [count, setCount] = useState<number>(0)
const doubled = useMemo(() => count * 2, [count])
return <div>Doubled count: {doubled}</div>
}
watch
のマッピング
Vue.jsのwatch
関数は、リアクティブなデータの変更を監視し、変更が検出されたときにコールバック関数を実行します。
<template>
<div>{{ message }}</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const count = ref<number>(0)
const message = ref<string>('')
watch(count, (newValue, oldValue) => {
message.value = `Count has changed from ${oldValue} to ${newValue}!`
})
function incrementCount() {
count.value += 1
}
</script>
この例では、count
の値が変わるたびに、message
が更新されます。
Reactでの同等の実装
Reactでは、useEffect
フックを使用して、stateやpropsの変更を監視し、変更があった場合に副作用を実行することができます。
React の useEffect
フックは、副作用 (side effects) を実行するためのものでこれには、データのフェッチ、手動でのDOM操作、サブスクリプションの設定、タイマーの設定などが含まれます。useEffect
は、コンポーネントのレンダリングが完了した後に動作します。
特定の state または props の変更を監視する場合、useEffect
の第二引数として依存配列を渡します。
import React, { useState, useEffect } from 'react'
const MessageDisplay: React.FC = () => {
const [count, setCount] = useState<number>(0)
const [message, setMessage] = useState<string>('')
useEffect(() => {
const previousCount = count - 1; // この例では単純に1減算することで前の状態を再現しています。
setMessage(`Count has changed from ${previousCount} to ${count}!`)
}, [count])
function incrementCount() {
setCount(prevCount => prevCount + 1)
}
return (
<div>
<div>{message}</div>
<button onClick={incrementCount}>Increment</button>
</div>
)
}
違いと注意点
- 実行タイミング:
- Vue.js の
watch
は、監視しているプロパティが変更されたときに即座に実行されます。 - React の
useEffect
は、レンダリングが完了した後に実行されます。
- Vue.js の
- 依存配列:
- React の
useEffect
では、特定の変数やプロパティの変更を監視するには、それらを依存配列にリストアップする必要があります。この配列が提供されないと、useEffect
は毎レンダリング後に実行されます。
- React の
- クリーンアップ:
useEffect
は、副作用をクリーンアップするための機能も提供しています。useEffect
の中で関数を return すると、その関数はコンポーネントがアンマウントされる前や、次のuseEffect
が実行される前に実行されます。- Vue.js では、
watch
はクリーンアップ関数を直接提供していませんが、onBeforeUnmount
ライフサイクルフックを使用して同様のことを行うことができます。
ライフサイクルとそのマッピング
Vue.jsのライフサイクルメソッドは、Composition APIでonMounted
, onUpdated
などの関数として提供されています。onMounted
は、コンポーネントがDOMにマウントされた後に実行されるフックです。これは主に、DOM要素へのアクセスや、外部APIの呼び出し、初期設定などの目的で使用されます。onUpdated
は、コンポーネントのデータが変更されて再レンダリングが行われた後に実行されるフックです。
<script setup lang="ts">
import { ref, onMounted, onUpdated } from 'vue'
const count = ref(0)
onMounted(() => {
console.log("Component is mounted!")
})
onUpdated(() => {
console.log("Component is updated!")
})
</script>
Reactでの同等の実装
これに対して、ReactにはuseEffect
というフックがあります。useEffect
の依存配列が空の場合([]
)、その効果はコンポーネントがマウントされた後に一度だけ実行されます。クリーンアップ関数を返すと、コンポーネントがアンマウントされる際に実行されます。useEffect
の依存配列に特定の状態やpropsを指定することで、その値が変更されるたびに効果が実行されます。
またuseEffect
内で関数をreturn
すると、それがクリーンアップ関数として認識されます。
これはコンポーネントがアンマウントされる前に実行されます。
import React, { useState, useEffect } from 'react'
const CounterButton: React.FC = () => {
const [count, setCount] = useState<number>(0)
useEffect(() => {
console.log("Component is mounted!")
return () => {
console.log("Component will unmount!")
}
}, [])
useEffect(() => {
if (count > 0) {
console.log("Count is updated!")
}
}, [count])
return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
}
この例では、最初のuseEffect
はコンポーネントのマウントとアンマウント時に実行されるロジックを扱います。2つ目のuseEffect
は、count
の値が更新されたときに実行されるロジックを示しています。
Vue.jsのcomposables関数とReactのカスタムフック
Vue.jsのComposition APIは、コンポーネントの再利用性を高めるための強力なツールとして登場しました。この中で特にcomposables関数は、複数のコンポーネントで再利用できるロジックの集合を提供します。Reactでは、同様の目的のためにカスタムフックが存在します。
Vue.jsのcomposables関数
Composition API内で、composablesは再利用可能な関数として定義されます。これらの関数は、特定のロジックやステートをカプセル化し、異なるコンポーネントで簡単に使えるようにします。
<script lang="ts">
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment
}
}
</script>
コンポーネントでの利用
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useCounter } from './path-to-useCounter';
const { count, increment } = useCounter();
</script>
Reactのカスタムフック
Reactでもカスタムフックを使用して、再利用可能なロジックやステートをカプセル化することができます。カスタムフックは関数として定義され、内部でReactの既存のフックを使用することが一般的です。
import { useState } from 'react';
export function useCounter() {
const [count, setCount] = useState(0);
function increment() {
setCount(prevCount => prevCount + 1);
}
return {
count,
increment
}
}
コンポーネントでの利用
import React from 'react';
import { useCounter } from './path-to-useCounter';
function CounterComponent() {
const { count, increment } = useCounter();
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
export default CounterComponent;
まとめ
Vue.jsとReactで比較すると色々違いがあって面白いですね
間違い等があれば教えていただけると幸いです