Vue3系から導入されたComposition APIを使用することで、VueでもReactのようにカスタムフックを用いたロジックの切り分けができるようになりました。
今回はVue3系のComposition APIで使えるカスタムフックを利用したロジックの切り分け方について、解説してみました。
Composition APIについて簡単に解説
カスタムフックの説明に入る前にComposition APIについて、従来の書き方であるOptions APIと比較しながら簡単に解説していきます。
Options APIでの書き方
今回はサンプルコードとして、ボタンをクリックした回数を表示するコンポーネントを用意しました。
Options APIで実装すると、以下のようになります。
<template>
<button @click="increment">Counter</button>
<p>{{ count }}</p>
</template>
<script>
export default {
data() {
return {
count: 0,
}
},
methods: {
increment () {
this.count++;
}
},
}
</script>
Options APIではdata()
の中でstateの定義を行い、methods
の中に関数を記述することで、ロジックの実装を行います。
Composition APIでの書き方
Optioons APIで実装したボタンコンポーネントを今度はComposition APIで実装してみます。
Vue3系から使用可能なComposition APIで実装を行うと以下のようになります。
<template>
<button @click="increment">Counter</button>
<p>{{ count }}</p>
</template>
<script>
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const count = ref(0);
const increment = () => count.value++;
return {
count,
increment,
};
},
});
</script>
Composition APIではsetup関数の中に、stateの定義とロジックをまとめて処理を記述することが可能です。
関数の中に状態の保持とロジックをまとめて処理を記述できるので、stateの定義や関数の外出しが簡単にできるという特徴があります。
状態管理を方法に関してもComposition APIではref
やreactive
という関数を利用することでstate管理を行うことができます。
少々本題から脱線しますが、自分はrefとreactiveは以下のように使い分けています。
- ref・・・単一の値の状態管理を行いたいときに使用。
- reactive・・・オブジェクトを用いて複数の値の状態を管理したいときに使用。
じゃあ、カスタムフックって一体何なのかって話
では本題です。
カスタムフックとは一体何なのでしょうか?
カスタムフックとは、stateの定義や操作に関する処理を別のファイルに切り出して定義した関数のことです。
カスタムフックはReactでビュー(ユーザーの目に見える部分)とロジックを切り出すため使用されるテクニックなのですが、Composition APIの登場によって、ロジックの切り分けが簡単にできるようになったVue3系でもカスタムフックが使えるようになりました。
カスタムフックに関して、Reactの公式ドキュメントでは以下のように説明されています。
自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。
参考サイト: 独自フックの作成
トーストコンポーネントを作りながらカスタムフックを理解する
実際にどうやって処理を書いていくのか文章だけだとイメージが湧きづらいかと思うので、今回はカスタムフックを用いてシンプルなトーストコンポーネントを作ってみました。
サンプルコードはGitHubに掲載していますので、ご覧ください。
トーストのロジックを別ファイルに切り出す
まずトーストのstate操作を行うロジックを別ファイルに切り出します。
フックであると一目で分かるように、use-toast.ts
というファイルを作成して、関数の定義を行います。
import { ref } from 'vue';
export const useToast = () => {
const isToastActive = ref(false);
const handleClick = () => {
isToastActive.value = !isToastActive.value;
};
const closeToast = () => {
isToastActive.value = false;
};
return {
isToastActive,
handleClick,
closeToast,
};
};
フックの命名規則ですが、Reactだと慣例としてuse〇〇という名前でつける決まりがあるので、それに習ってuseToast
と命名しました。
コンポーネント側でstateや関数を分割代入で呼び出せるようにするために、定数やロジックをオブジェクトとして返却しています。
今回は不要なので指定していませんが、引数を設定する必要があればフックには引数を渡せるようにしても構いません。
APIのリクエスト結果に対して型をつけたい時はジェネリクスを用いて、レスポンス結果のオブジェクトを渡せるようにして、汎用性を持たせる場合もあると思います。
実装したカスタムフックをコンポーネントから呼び出して使用する
<template>
<button @click="handleClick">トーストを表示</button>
<Toast :is-toast-active="isToastActive" @close-toast="closeToast">
<h2>Toastのサンプル</h2>
<p>トースト🍞</p>
</Toast>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useToast }from './lib/use-toast';
import Toast from './components/Toast.vue';
export default defineComponent({
components: {
Toast,
},
setup() {
const { isToastActive, handleClick, closeToast } = useToast();
return {
isToastActive,
handleClick,
closeToast,
}
}
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
h1,h2 {
padding: 0;
margin: 0;
}
</style>
フックが完成したら後はコンポーネント側で呼び出すだけです。
コンポーネントではuseToastというフックをインポートしています。
フックからstateやロジックを呼び出す処理しか書いていないので、全体的に処理がすっきりコードが読みやすくなりました。
ちなみに、トーストコンポーネントはこんな感じで実装してみました。
<template>
<transition name="bottom">
<div v-if="isToastActive" class="toast">
<button @click="$emit('closeToast')" class="toast-close-btn">×</button>
<div class="toast-container">
<slot />
</div>
</div>
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
isToastActive: {
type: Boolean,
required: true,
}
},
emits: ['closeToast'],
});
</script>
<style scoped>
.toast {
position: fixed;
bottom: 30px;
right: 30px;
transition: all 0.6s;
border: 1px solid rgba(0,0,0,.1);
width: 100%;
max-width: 420px;
box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%);
}
.toast-close-btn {
position: absolute;
top: 0;
right: 0;
background: none;
border: none;
cursor: pointer;
color: red;
font-size: 30px;
}
.toast-container {
padding: 8px;
}
/* アニメーション */
.bottom-enter-active, .bottom-leave-active {
transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms;
}
.bottom-enter-from {
transform: translateY(100vh);
}
.bottom-enter-from, .bottom-leave-to {
opacity: 0;
}
.bottom-leave-active {
transition: all 0.4s;
transform: translateY(300px);
}
</style>
カスタムフックを利用するメリット
シンプルなトーストコンポーネントを例にカスタムフックについて解説をしましたが、コンポーネントにロジックを直書きするのではなくカスタムフックとして切り出すとどのようなメリットがあるのでしょうか?
カスタムフックを利用するメリットとしては、以下のようなメリットが挙げられます。
- 複数のコンポーネントで使用する可能性がある処理をまとめられる
- ロジックをコンポーネントから切り離すことで、コードの可読性が上がる
ビューとロジックを別ファイルに切り出すことで、コンポーネントの可読性を高められるんだなー程度に考えておけば間違いはないと思います。
カスタムフックとして扱ってはいけないケース
簡単にコンポーネントからロジックを切り出すことができるカスタムフックですが、カスタムフックとして扱ってはいけないケースがあります。
扱ってはいけないケースですが、グローバルステートを扱うときです。
グローバルステートをカスタムフックとして扱ってはいけない理由ですが、カスタムフックで管理するstateをコンポーネント同士が共有することはないからです。
stateをコンポーネント同士が共有することがないとはどういうことか説明するための例を用意してみました。
<template>
<button @click="handleClick">トーストを表示(Hoge.vue)</button>
<Toast :is-toast-active="isToastActive" @close-toast="closeToast"> <h2>Toastのサンプル Hoge.vue</h2>
<p>トースト(Hoge)🍞</p>
</Toast>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useToast }from '../lib/use-toast';
import Toast from './Toast.vue';
export default defineComponent({
components: {
Toast,
},
setup() {
const { isToastActive, handleClick, closeToast } = useToast();
return {
isToastActive,
handleClick,
closeToast,
}
}
});
</script>
<template>
<button @click="handleClick">トーストを表示(Fuga.vue)</button>
<Toast :is-toast-active="isToastActive" @close-toast="closeToast">
<h2>Toastのサンプル Fuga.vue</h2>
<p>トースト(Fuga)🍞</p>
</Toast>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useToast } from '../lib/use-toast';
import Toast from './Toast.vue';
export default defineComponent({
components: {
Toast,
},
setup() {
const { isToastActive, handleClick, closeToast } = useToast();
return {
isToastActive,
handleClick,
closeToast,
}
}
});
</script>
<template>
<Hoge />
<Fuga />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import Hoge from './components/Hoge.vue';
import Fuga from './components/Fuga.vue';
export default defineComponent({
components: {
Hoge,
Fuga,
},
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
h1,h2 {
padding: 0;
margin: 0;
}
</style>
先程のトーストコンポーネントをHoge.vue
とFuga.vue
2つのコンポーネントに分けて、App.vue
で読み込ませてみました。
トーストの挙動を確認すると、以下のような挙動になります。
- トーストを表示(Hoge.vue)をクリック・・・Toastのサンプル Hoge.vueが表示される
- トーストを表示(Fuga.vue)をクリック・・・Toastのサンプル Fuga.vueが表示される
上記の挙動から、Hoge.vue
とFuga.vue
は別々のstate呼び出して、別々のstateを切り替える処理を行なっていることが分かります。
このことから、同じフックを使ってるコンポーネント同士がstateを共有することはないということが言えます。
グローバルステートは原則として、アプリケーション内のどのコンポーネントからもstateの参照、操作ができないといけないので、フックとは違うことが分かるかと思います。
Vue3系のComposition APIでグローバルステートを扱う手段としては、Options APIの頃からあったVuexに加えProvide・injectパターンなども使用できますが、これらを用いて状態管理・操作を行うときは、カスタムフックではなくstoreとして扱った方がいいと思います。
まとめ
- カスタムフックは、コンポーネントからロジックを切り離すときに使用する
- フックは慣例として、
use〇〇
という名前で命名する必要がある - カスタムフックで管理するステートをコンポーネント同士が共有することはない
- グローバルステートの保持・操作を行うときはカスタムフックとして扱ってはいけない
今回の内容で記事を執筆するにあたって、Vueのカスタムフックについて解説している記事がなかなか見つからなかったので、Reactのカスタムフックについて解説した記事も参考にさせていただきました。
https://ja.reactjs.org/docs/hooks-custom.html
https://zenn.dev/luvmini511/articles/df410f137d1e21
https://qiita.com/cheez921/items/af5878b0c6db376dbaf0
読んでいただきまして、ありがとうございました。