Vue.jsからReactへの移行: Composition APIの機能をReactで実装する方法

Vue.jsからReactへの移行を考えていく中で、Vue.jsのComposition APIにおける機能やパターンをReactでどのように再現できるか疑問に思うことが多かったのでまとめていきたいと思います。

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はメモ化された値を返すフックでこれは、指定された依存関係リスト内の変数が変更されたときのみ再計算されます。これにより、不要な再計算を避けることができます。

したがって、useStateuseMemoを組み合わせることで、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>
  )
}

違いと注意点

  1. 実行タイミング:
    • Vue.js の watch は、監視しているプロパティが変更されたときに即座に実行されます。
    • React の useEffect は、レンダリングが完了した後に実行されます。
  2. 依存配列:
    • React の useEffect では、特定の変数やプロパティの変更を監視するには、それらを依存配列にリストアップする必要があります。この配列が提供されないと、useEffect は毎レンダリング後に実行されます。
  3. クリーンアップ:
    • 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で比較すると色々違いがあって面白いですね
間違い等があれば教えていただけると幸いです

コメントを残す

入力エリアすべてが必須項目です。メールアドレスが公開されることはありません。

内容をご確認の上、送信してください。