この記事は
これまでReactを触ってきた私がVueを触ることになり、Vue初心者として学習したことの記録です。
主に24年12月現在のステート管理のベストプラクティスって何だろってとこをまとめていきます。
ちなみに環境は「vue3」,「Nuxt3」を前提とした話になっています。
始まりはコンポーネントの整理がしたかった
実装を進めていくと次第にコード量が膨らみ、コンポーネントを分割するかとなったのですが、ふとあることが気になりました。
コンポーネント名ってどうやって決まってるの?
Reactだとクラス名がつけられるのでそれを呼び出せばOK
Vueでは基本的に「ファイル名」が呼び出し時の名称として使われるそうで、しかもNuxtでは、components/ディレクトリ内に配置されたコンポーネントが自動的にインポートされるため痕跡が表からは消されている状態となる。
じゃあ、ページ固有の子コンポーネントもcomponents/配下に置いたほうがいいの?
それは違うようで、再利用しない子コンポーネント(親専用のもの)は自動インポート対象にせず、親と同じディレクトリ内で管理することが推奨されます。
これにより、components/配下が溢れかえることなく整理することができます。よかった。
コンポーネントを分割したらデータの受け渡しが発生する
コンポーネントを分割することで、役割が明確になり、可読性も上がり、再利用しやすなった。
しかし、コンポーネントを分割するということは、データの受け渡しが発生するということなわけで
「Vueにはどんなデータの受け渡し方があるんだろう」
と気になり、調べてみました。
ステート管理
Props/Emits
概要
- Props: 親から子へデータを渡すために使用します
- Emits: 子から親へイベントを送信してデータや通知を渡すために使用します
適用シチュエーションとメリット
- 親子関係が明確な場合
- シンプルなデータフロー(親→子、子→親)を実現したい場合
- データフローが明確になる
例
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue';
const parentMessage = 'Hello from Parent';
function handleUpdate(newMessage) {
console.log('Received from child:', newMessage);
}
</script>
<template>
<ChildComponent :message="parentMessage" @update-message="handleUpdate" />
</template>
<script setup lang="ts">
const props = defineProps({
message: String
});
const emit = defineEmits(['update-message']);
function updateMessage() {
emit('update-message', 'Hello from Child');
}
</script>
<template>
<div>
<p>{{ props.message }}</p>
<button @click="updateMessage">Update Message</button>
</div>
</template>
思ったこと
呼び出す際に何が必要か、不足はなにかを静的にチェックしてくれるのはとても助かる。
どこから渡ってきたかが明確なのもミスが起こりにくい。
ただ、コンポーネントの階層が深くなるとバケツリレーが煩わしくなるのが大きな難点。
defineModel
概要
- Vue 3.4以降で利用可能になった
- propsとemitsの組み合わせを簡略化し、双方向バインディング(v-model)をシンプルに実現する
適用シチュエーション
- 双方向バインディングが必要な場合(例: 入力フォーム)
- 親子間のデータ同期を簡潔に実現したい場合
例
<script setup lang="ts">
import InputField from './InputField.vue';
const userName = ref('');
</script>
<template>
<InputField v-model="userName" />
</template>
<script setup lang="ts">
const modelValue = defineModel<string>('modelValue');
</script>
<template>
<input type="text" v-model="modelValue" />
</template>
思ったこと
ステート管理のコードが簡潔に整理され役割が明確になる。
双方向なデータフローが必要な場合 → defineModel
単方向なデータフロー → PropsとEmits
と役割を分けることでそのデータの性質を理解しすくなり可読性が上がるんじゃないかな。
Provide/Inject
概要
- 親コンポーネントが値や関数を「提供(provide)」し、任意の深さの子コンポーネントでそれを「注入(inject)」する
- Propsの代わりに、階層が深い場合でもデータを渡すことが可能です
適用シチュエーション
- 深くネストされたコンポーネントツリー内でデータ共有が必要な場合
- コンポーネント間でステート管理を簡素化したい場合
例
<script setup lang="ts">
provide('message', 'Hello from Parent');
</script>
<template>
<ChildComponent />
</template>
<script setup lang="ts">
const message = inject('message');
</script>
<template>
<p>{{ message }}</p>
</template>
思ったこと
バケツリレーを回避することができる!
けど、コンパイル時に親コンポーネントが正しくProvideしているかどうかをチェックしないので、エラーがわかりにくい。
Injectされた値がどこから来ているのかがコード上で明示されないため、コードの可読性や保守性が低下する可能性がある。
useState
概要
「Nuxt3」で使用可能なステート管理の選択肢。
キーと初期化関数を指定してリアクティブなステートを作成し、コンポーネント間で共有できます。
親子間のみに限定されず、グローバルなステート管理にも利用可能。
適用シチュエーション
- useStateはキーを基にステートを作成するため、アプリ全体で一貫したステート管理が可能
- 複数のコンポーネントでステートを共有する際に便利
- PropsやProvide/Injectでは親子関係が必要ですが、useStateはその制約がない
例
<script setup lang="ts">
const counter = useState('counter', () => 0);
</script>
<template>
<div>
Counter: {{ counter }}
<button @click="counter++"> + </button>
<button @click="counter--"> - </button>
</div>
</template>
思ったこと
グローバルなステートを使いたい場合に有力な手段。
メモリ上にのみ保持され、セッションストレージやローカルストレージにデータを保存しないので、ページのリロードやブラウザを閉じて再度開いた場合には状態が初期化される。
まとめてみた
手法 | 概要 | 有効なケース | メリット | デメリット |
---|---|---|---|---|
Props/Emits | 親から子へデータを渡す(Props)、子から親へイベントを通知する(Emits) | 親子間の明確なデータフローが必要な場合 | データフローが明確で、単方向データバインディングを実現 | 深いネストではProp drillingになる可能性がある |
defineModel | 双方向バインディングを簡潔に実現するためのマクロ | 双方向バインディングが必要な場合(フォーム入力など) | コードが簡潔になり、双方向バインディングが容易 | 親子間に限定されるため、兄弟や非直系コンポーネントでは利用不可 |
Provide/Inject | 親コンポーネントが値や関数を提供し、任意の深さの子コンポーネントで注入可能 | 深いネスト構造や非直系コンポーネント間でデータ共有が必要な場合 | Prop drillingを回避できる。任意階層で利用可能 | 提供元が不明瞭になることがあり、トレーサビリティが低下する可能性 |
useState | ステートを管理し、そのステートに基づいてコンポーネントを再レンダリングする | グローバルなステート管理が欲しい場合 | 親子間の制限なくステートの共有が可能 | ステート管理が複雑になると、コードが煩雑になりやすい |
結論
調べ初めは利用をどれかに統一したほうがわかりやすくなるんじゃないかとも思いましたが、けっこう棲み分けができていて
用途によって使い分けるほうが可読性が上がるだろうなと思っています。
ただ、Provide/InjectやuseStateはとくに、宣言を適切に管理しないと
「今何が呼び出せる状態なのか」、「どの時点の値が入っているのか」が曖昧になってしまう点が気を付けていきたいとこですね。