はじめに
この記事では、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 は、どちらも子から親へ出来事を伝えるために使います。
違いをまとめると次のとおりです。
- Vue は
emit("eventName", payload)でイベントを発火する - React は
onEventName(payload)のように渡された関数を呼ぶ - Vue は親側で
@event-name="handler"と受ける - React は親側で
onEventName={handler}と渡す - どちらも子が親の state を直接変更するわけではない
Vue はイベントとして通知する形です。
React は関数を渡して呼ぶ形です。
この違いを押さえると、Vue の emit も React のコールバック props も、同じ「子から親へ伝える」仕組みとして整理しやすくなります。
Vue の入力フォームで親子間の値更新を扱う場合は、別記事「Vueのv-modelは便利だがReactのvalueとonChangeはなぜ分かりやすいのか」も関連します。
Vue と React のコードを読む順番の違いについては、別記事「ReactとVueの書き方の違い:同じ画面でも読む順番が変わる」でも整理しています。