1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

VueのemitとReactのコールバックpropsは何が違うのか

1
Posted at

はじめに

この記事では、Vue の emit と React のコールバック props の違いを整理します。

どちらも、子コンポーネントから親コンポーネントへ「何かが起きた」と伝えるために使います。

ただし、書き方と読み方はかなり違います。

  • Vue は子が emit でイベントを発火する
  • React は親が関数を props として渡し、子がその関数を呼ぶ

この記事では、同じ親子間通信を Vue と React で比べながら、何が違って見えるのかを整理します。

先に結論

先に結論を書くと、違いは次のとおりです。

  • Vue は「イベント名で通知する」感覚が強い
  • React は「関数を渡して呼ぶ」感覚が強い
  • Vue はテンプレートで @event-name と受ける
  • React は JSX で onEventName={handler} と渡す
  • どちらも子が親の state を直接変更するわけではない

つまり、目的は近いです。

ただし、Vue はイベントとして見え、React は関数呼び出しとして見えます。

Vueはemitで子から親へイベントを通知する

Vue では、子コンポーネントから親コンポーネントへ通知したいときに emit を使います。

親から子へ値を渡すときは props です。

子から親へ出来事を伝えるときは emit です。

例えば、子コンポーネントのボタンを押したら、親コンポーネント側でカウントを増やす例を考えます。

子コンポーネントは、defineEmits で発火するイベントを宣言します。

<script setup lang="ts">
const emit = defineEmits<{
  increment: []
}>()

const onClick = (): void => {
  emit("increment")
}
</script>

<template>
  <button type="button" @click="onClick">増やす</button>
</template>

親コンポーネントは、@increment でイベントを受け取ります。

<script setup lang="ts">
import { ref } from "vue"
import CountButton from "./CountButton.vue"

const count = ref(0)

const incrementCount = (): void => {
  count.value += 1
}
</script>

<template>
  <p>{{ count }}</p>
  <CountButton @increment="incrementCount" />
</template>

子は count を直接知りません。

子は「ボタンが押された」と通知しているだけです。

実際に count を増やすかどうかは、親コンポーネントが決めています。

Reactは親から関数を渡して子が呼ぶ

React には Vue の emit に相当する専用の仕組みはありません。

子から親へ何かを伝えたい場合は、親が関数を props として子に渡します。

子は、その関数を呼び出します。

Vue の emit("increment") と同じカウント処理を React で書くと、次のようになります。

type CountButtonProps = {
  onIncrement: () => void
}

const CountButton = ({ onIncrement }: CountButtonProps) => (
  <button type="button" onClick={onIncrement}>
    増やす
  </button>
)

親コンポーネントでは、state と更新関数を持ちます。

import { useState } from "react"

const ParentComponent = () => {
  const [count, setCount] = useState(0)

  return (
    <>
      <p>{count}</p>
      <CountButton onIncrement={() => setCount((count) => count + 1)} />
    </>
  )
}

React では、子が onIncrement を呼びます。

親は onIncrement に渡した関数の中で count を更新します。

この例のように処理が短い場合は、JSX の中でそのまま関数を渡しても十分読みやすいです。

処理が長くなる場合や、同じ処理を複数箇所で使う場合は、handleIncrement のような関数に切り出すと読みやすくなります。

なお、useCallback は常に必要なものではありません。
子コンポーネントを memo していて関数参照の安定が効く場合など、必要になったところで検討すれば十分です。

Vue のようにイベント名を発火するのではなく、JavaScript の関数を渡して呼ぶ形です。

同じことをしているが見え方が違う

Vue と React は、どちらも子が親の state を直接変更しているわけではありません。

共通しているのは次です。

  • state は親が持つ
  • 子は操作されたことを親に伝える
  • 親が state を更新する
  • 更新された値が子や画面に反映される

違うのは、伝え方です。

Vue ではイベント名が中心になります。

<CountButton @increment="incrementCount" />

React では関数名が中心になります。

<CountButton onIncrement={() => setCount((count) => count + 1)} />

Vue は「子が increment イベントを発火し、親がそれを受ける」と読みます。

React は「親が onIncrement 関数を渡し、子がそれを呼ぶ」と読みます。

目的は近いですが、読むときの入口が違います。

データを渡す場合の違い

子から親へデータを渡したい場合も、考え方は同じです。

Vue では、emit の第2引数以降に payload を渡します。

<script setup lang="ts">
type User = {
  id: number
  name: string
}

defineProps<{
  users: User[]
}>()

const emit = defineEmits<{
  selectUser: [id: number]
}>()

const selectUser = (id: number): void => {
  emit("selectUser", id)
}
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <button type="button" @click="selectUser(user.id)">
        {{ user.name }}
      </button>
    </li>
  </ul>
</template>

親は @select-user で受け取ります。

<script setup lang="ts">
import { ref } from "vue"
import UserList from "./UserList.vue"

const users = [
  { id: 1, name: "山田" },
  { id: 2, name: "佐藤" }
]

const selectedUserId = ref<number | null>(null)

const handleSelectUser = (id: number): void => {
  selectedUserId.value = id
}
</script>

<template>
  <UserList :users="users" @select-user="handleSelectUser" />
  <p>選択中のID: {{ selectedUserId }}</p>
</template>

React では、親から渡された関数に引数を渡します。

type User = {
  id: number
  name: string
}

type UserListProps = {
  users: User[]
  onSelectUser: (id: number) => void
}

const UserList = ({ users, onSelectUser }: UserListProps) => (
  <ul>
    {users.map((user) => (
      <li key={user.id}>
        <button type="button" onClick={() => onSelectUser(user.id)}>
          {user.name}
        </button>
      </li>
    ))}
  </ul>
)

親は onSelectUser に関数を渡します。

import { useState } from "react"

const users = [
  { id: 1, name: "山田" },
  { id: 2, name: "佐藤" }
]

const ParentComponent = () => {
  const [selectedUserId, setSelectedUserId] = useState<number | null>(null)

  return (
    <>
      <UserList users={users} onSelectUser={setSelectedUserId} />
      <p>選択中のID: {selectedUserId}</p>
    </>
  )
}

この例では、受け取った id をそのまま state に入れるだけなので、setSelectedUserId を直接渡しています。

選択時にログを送る、別の値に変換する、画面遷移する、といった処理が増えるなら、handleSelectUser を定義して渡す方が自然です。

Vue では emit("selectUser", id)

React では onSelectUser(id)

同じように見えますが、Vue はイベント発火、React は関数呼び出しとして読むと整理しやすいです。

Vueはイベント名、Reactは関数名で読む

Vue の親側では、テンプレートにイベント名が出ます。

<UserList @select-user="handleSelectUser" />

子側では、次のようにイベントを発火します。

emit("selectUser", id)

Vue では、子側のTypeScriptでは camelCase、親側のテンプレートでは kebab-case で読む場面があります。

  • 子のTypeScript側では selectUser
  • 親のテンプレート側では @select-user

一方で React は、親側から渡した props 名がそのまま子側にも出ます。

<UserList onSelectUser={setSelectedUserId} />
onSelectUser(user.id)

React は JavaScript / TypeScript の関数呼び出しとして追いやすいです。

Vue はテンプレート上で「このイベントを受けている」と見やすいです。

ここは好みというより、読み方の違いです。

複数の値はオブジェクトにまとめると読みやすい

Vue でも React でも、複数の値を渡したい場面があります。

Vue なら、次のように複数引数で渡せます。

emit("updateUser", id, name)

React でも、関数に複数引数を渡せます。

onUpdateUser(id, name)

ただし、値が増えるならオブジェクトにまとめた方が読みやすいです。

Vue なら次の形です。

emit("updateUser", {
  id,
  name
})

React なら次の形です。

onUpdateUser({
  id,
  name
})

TypeScript でも payload の型を付けやすくなります。

type UpdateUserPayload = {
  id: number
  name: string
}

引数の順番に依存しないため、レビューでも読みやすくなります。

深い階層ではどちらもつらくなる

Vue の emit も React のコールバック props も、直接の親子関係では分かりやすいです。

ただし、階層が深くなるとつらくなります。

Vue で何段も emit を上に伝えていくと、どこで何を受けているのか追いづらくなります。

React でも何段も関数を props で渡していくと、props drilling になりやすいです。

そのような場合は、次のような方法を検討します。

  • 状態を共通の親に持ち上げる
  • composable や custom hook に状態管理を切り出す
  • Pinia や React Context などを使う
  • 必要なら状態管理ライブラリを使う

emit やコールバック props は、すべてのコンポーネント間通信を解決するものではありません。

直接の親子間で使う基本形として捉えると分かりやすいです。

まとめ

Vue の emit と React のコールバック props は、どちらも子から親へ出来事を伝えるために使います。

違いをまとめると次のとおりです。

  1. Vue は emit("eventName", payload) でイベントを発火する
  2. React は onEventName(payload) のように渡された関数を呼ぶ
  3. Vue は親側で @event-name="handler" と受ける
  4. React は親側で onEventName={handler} と渡す
  5. どちらも子が親の state を直接変更するわけではない

Vue はイベントとして通知する形です。

React は関数を渡して呼ぶ形です。

この違いを押さえると、Vue の emit も React のコールバック props も、同じ「子から親へ伝える」仕組みとして整理しやすくなります。

Vue の入力フォームで親子間の値更新を扱う場合は、別記事「Vueのv-modelは便利だがReactのvalueとonChangeはなぜ分かりやすいのか」も関連します。

Vue と React のコードを読む順番の違いについては、別記事「ReactとVueの書き方の違い:同じ画面でも読む順番が変わる」でも整理しています。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?