
こんにちは、クライアント開発チームの田中智之です!
さて、ビザスクではVue.jsを利用してフロントエンドを実装していますがcomposablesの使い方、難しくないでしょうか?
自分自身の解像度を高めるためにも、今回はVue.jsの中でもcomposablesに焦点を当てた記事を書いてみることにしました!
SFC(Single File Component)についてcomposabelsの話に入る前に、SFCについて触れさせてください。SFCとは単一ファイルコンポーネントの章にあるように、単一ファイルでコンポーネントの振る舞いを実現するvueの記法になります。
現代の UI 開発では、コードベースを互いに織り交ぜる 3 つの巨大なレイヤーに分割するのではなく、それらを疎結合なコンポーネントに分割して構成する方がはるかに理にかなっていることが分かっています。コンポーネント内では、そのテンプレート、ロジック、およびスタイルが本質的に結合されており、それらを連結することで、実際にコンポーネントがよりまとまり、保守しやすくなります。
上記からはSFC内でロジックを閉じることを推奨しているようにも読み取れます。
composablesについてひとまずcomposablesが生まれた背景を理解するのが良さそうです。
vue公式ドキュメントのコンポーザブルの章を見てみます。
フロントエンドアプリケーションを構築するとき、共通のタスクのためにロジックを再利用しないといけないことがよくあります。例えば、多くの箇所で日付をフォーマットする必要があるので、そのための再利用可能な関数を抽出します。このフォーマッターは状態のないロジックをカプセル化し、ある入力を受け取ったら即座に期待される出力を返します。状態のないロジックを再利用するためのライブラリーはたくさんあります。例えば lodash や date-fns などは聞いたことがあるかも知れません。対照的に、状態のあるロジックは時間とともに変化する状態の管理が伴います。ページ上のマウスの現在位置をトラッキングするようなものがシンプルな例といえます。実際のシナリオでは、タッチジェスチャーやデータベースへの接続状態など、より複雑なロジックになる場合もあります。
なるほど、ライブラリを交えつつ説明しているのがポイントでしょうか。状態を持った再利用可能なロジックをcomposablesと理解して良さそうです!
inputのv-modelでcomposabelsを試す早速、よくある例でcomposabelsを試してみましょう。
inputのv-modelでcomposabelsを試してみます。
// Input.vue<script setup>import { ref } from 'vue'const message = ref('Awesome Input Message')</script><template> <h1>{{ message }}</h1> <input v-model="message" /></template>これをcomposabelsにしてみます。
import { ref } from 'vue'// composabelsはuse~という命名にするのが慣習export const useInputMessage = () => { const message = ref('Awesome Input Message') return { message: message }}// Input.vue<script setup>const { message } = useInputMessage();</script><template> <h1>{{ message }}</h1> <input v-model="message" /></template>いかがでしょうか。inputとrefは結合度が高いためSFCの考えである下記と乖離してしまうように思います。
コンポーネント内では、そのテンプレート、ロジック、およびスタイルが本質的に結合されており、それらを連結することで、実際にコンポーネントがよりまとまり、保守しやすくなります。
もう少し、抽象的で再利用可能なロジックをcomposablesにしたいところです!
composabelsを試す「最新」と「全件」の表示を切り替えるcomposabels*1を考えてみます。
今回は歴代ポケットモンスターを世代ごとに分類分けする仕様を例としてみます。最終的なUIのイメージは下記です。

以下がcomposabelsです。
コードの肝は以下でしょうか。
T extends Record<string, unknown> & { version: number }を受け取るMaybeRefで定義することでreactiveを受け取れるようにしておくexport const useVersion = < T extends Record<string, unknown> & { version: number },>( records: Readonly<MaybeRef<T[] | undefined>>, // `Readonly`で利用側には`immutable`であることを明示しておく) => { /** 最新のversionか全件かに応じてフィルタするcomposables */ const displayVersion = ref<"latest" | "all">("latest"); const showAll = () => (displayVersion.value = "all"); const showLatest = () => (displayVersion.value = "latest"); return { records: computed(() => { if (!records.value) return []; const latest = Math.max( ...records.value.map((record) => record.version), ); return displayVersion.value === "latest" ? records.value.filter((record) => record.version === latest) : records.value .filter((record) => record.version > 0) .sort((a, b) => (a.version < b.version ? 1 : -1)); }), displayVersion, showAll, showLatest, };};利用側は下記です。
// versionの分類分けは世代ごと(eg. version=1は第1世代)const { records, showAll, showLatest } = useVersion([ { id: 1, title: "ポケットモンスター 赤・緑", version: 1 }, { id: 2, title: "ポケットモンスター 青", version: 1 }, { id: 3, title: "ポケットモンスター ピカチュウ", version: 1 }, { id: 4, title: "ポケットモンスター 金・銀", version: 2 }, { id: 5, title: "ポケットモンスター クリスタルバージョン", version: 2 }, { id: 6, title: "ポケットモンスター ルビー・サファイア", version: 3 }, { id: 7, title: "ポケットモンスター エメラルド", version: 3, }, { id: 8, title: "ポケットモンスター ダイヤモンド・パール", version: 4, }, { id: 9, title: "ポケットモンスター プラチナ", version: 4, },]);....省略......今回はポケモンを例に取りましたが、Record<string, unknown> & { version: number }の型に当てはまるものは同様の分類分けが可能になります!
フロントエンドの実装では何かしら外部のロジックを利用することになるのがほとんどかと思います。簡単な図にすると下記のようなイメージでしょうか。

多くの場合、SFC内ではライブラリ,utils,composabelsを組み合わせてロジックを組み立てていくことになるわけですが、自分の中では、自前で実装することになるutils,composabelsを切り出す際には下記の2点が重要な観点なのではと思っています。
この二つは無関係ではなく、「抽象度が高い(≒再利用性が高い)」ため「信頼性の担保」が必要になる、ということになるのではないでしょうか?また、utilsとcomposabelsは状態を持っているか否かが大きな違いと言えるかもしれません。
そのため、切り出したcomposabelsはテストを書いた方がベターかと思いました。
むしろ、テストが書きづらい場合は以下の考慮が必要だという印象です!
composablesの処理が具体的すぎないかcomposablesではなく<script />内に記述すべきではないかコード整理のためのコンポーザブル抽出 に記載のある通り、
というような場合に、composabelsに切り出すというケースもありそうです!
状態を持ったロジックの切り出しにはレンダーレスコンポーネントという手段もあり、scoped slotsを用いてテンプレート内に閉じて状態を扱えるという長所があります。
ただしレンダーレスコンポーネントはレンダリングコストが高くなることが多いため、不必要に利用するべきではなさそうです。詳しくは下記を参照してください!
Vue.jsでは、例えばRuby on RailsのRails WayやReactにおけるReduxのように、明確な設計方針を提示してないため、開発者側で意図を汲み取り実装/設計を進める必要があります。
本記事はVue.jsのコアメンバーのお一人の記事からの一節を引用しつつ締めさせていただければと思います!
https://ublog.dev/blog/vue-is-approachable-ja
Vue.js は Approachable なのだ.何かを考える時,邪魔にならない.しかし,それと引き換えに実装者は少しの責任を負う.これはトレードオフなのだ.
過去には弊社エンジニアが書いたVue.js のソースコードを読んでみよう(ref/reactive 編) - VISASQ Dev Blog というようなVue.jsに関する記事もあるのでぜひ読んでみてください!
また、ビザスクではエンジニアの仲間を募集しています! 少しでもビザスク開発組織にご興味を持たれた方は、ぜひ一度カジュアルにお話ししましょう!
https://developer-recruit.visasq.works/
*1:全件表示時のソートは見逃してください
id:kinryu77jp
id:harada_visasq
id:waritocomatta
id:thebipul
id:kazutoichikawa
id:takumi_visasq
id:tkhttty
id:tanker-visasq
id:nken-htn
id:vterak
id:MasahiroSakai-visasq
id:ayaka_harada
id:mine-visasq
id:takahito-nakano-visasq
id:dtera-visasq
id:tomoyuki_tanaka_visasq
id:ryoyaanno
id:shkuma_vq
id:tsmkh3101
id:nakahara-visasq
id:r_tatsumi_vq
id:nanase_vq
id:sugi_visasq
id:onic_visasq
id:nattun_26
id:enpipi_visasq
id:macomaru-visasq
id:m_ishiii
id:satoshi_shimoyama_visasq
id:tumuzu
id:yoshik159753
id:shota_matsushita_visasq
id:kuramitsu-v
id:shiho-vq
id:kc-fujii
id:mizeeey_visasq
id:visasq_developers引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。