【Vue.js】Composition APIで状態を整理して管理する方法|setup関数の書き方と活用例

【Vue.js】Composition APIで状態を整理して管理する方法|setup関数の書き方と活用例 Vue.js

Vue 3のComposition APIは、コンポーネントのロジックを、インポートしたAPIの関数で組み立てる書き方です。関連する状態と処理を、機能ごとにまとめやすくなります。この記事では、基本のrefから、つまずきやすいreactiveの制限、<script setup>とsetup()関数の違い、Composableへの切り出しまでを整理します。

スポンサーリンク

Options APIとComposition APIの選び方

Vueのコンポーネントには、Options APIとComposition APIの2つの書き方があります。公式は、どちらも一般的なユースケースを十分に扱え、同じ仕組みの上に作られていると説明しています(Options APIは、Composition APIの上に実装されています)。どちらかが古い、劣っているということではありません。

  • Options API:data、methods、mountedのようなオプションで、ロジックを定義します。ビルドツールを使わない場合や、複雑でない場面(既存のページへの部分的な導入など)に向いています。
  • Composition API:インポートした関数でロジックを組み立てます。本格的なアプリを、単一ファイルコンポーネント(SFC)で作る場合に向いています。

学習のうちは、読みやすいと感じるほうで構いません。核になる概念は共通で、あとからもう一方を覚えられます。この記事では、Composition APIを説明します。

基本:refでリアクティブな変数を作る

SFCでは、Composition APIを<script setup>と一緒に使うのが一般的です。ref()に初期値を渡すと、リアクティブな変数ができます。スクリプトの中では.valueで読み書きし、テンプレートの中では.valueが不要です。

Vue:refの基本
<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++        // スクリプトの中では .value が必要
}
</script>

<template>
  <!-- テンプレートの中では、.value は不要(自動で取り出される) -->
  <p>カウント: {{ count }}</p>
  <button type="button" @click="increment">増やす</button>
</template>

<script setup>とsetup()関数の違い

Composition APIには、書き方が2つあります。<script setup>は、コンパイル時の変換を使って、トップレベルの変数や関数を自動でテンプレートに公開します。ボイラープレートが少なくなるため、SFCでは、こちらが一般的です。もう一方のsetup()関数は、オプションとして書き、テンプレートで使うものを自分でreturnする必要があります。

Vue:setup()関数を使う書き方
<script>
import { ref } from 'vue'

// setup() 関数を使う書き方:テンプレートで使うものは、return で公開する
export default {
  setup() {
    const count = ref(0)

    function increment() {
      count.value++
    }

    return { count, increment }      // return し忘れると、テンプレートから見えない
  }
}
</script>

テンプレートで .value が不要になるのは、トップレベルだけ

テンプレートでrefが自動で取り出されるのは、トップレベルのプロパティだけです。オブジェクトの中にネストしたrefは、取り出されません。

Vue:refの展開のルール
<script setup>
import { ref } from 'vue'

const count = ref(0)
const object = { id: ref(1) }
const { id } = object          // トップレベルの変数に取り出す
</script>

<template>
  {{ count + 1 }}          <!-- OK:トップレベルの ref は展開される → 1 -->
  {{ object.id + 1 }}      <!-- NG:ネストした ref は展開されない → "[object Object]1" -->
  {{ id + 1 }}             <!-- OK:取り出した変数なら展開される → 2 -->
  {{ object.id }}          <!-- 最終的な値としての出力は展開される → 1 -->
</template>

複数の状態をまとめる:reactiveと、その制限

関連する複数の値を、1つのオブジェクトにまとめたいときは、reactiveが使えます。

Vue:reactiveの基本
<script setup>
import { reactive } from 'vue'

const state = reactive({ name: '', age: 0 })

function reset() {
  state.name = ''
  state.age = 0
}
</script>

ただし、reactiveには、公式が明記している制限が3つあります。

JavaScript:reactiveの3つの制限
import { reactive } from 'vue'

// 制限① オブジェクト型にしか使えない(文字列・数値・真偽値は持てない)

// 制限② 丸ごと置き換えると、リアクティブな接続が失われる
let state = reactive({ count: 0 })
state = reactive({ count: 1 })        // 最初のオブジェクトは、もう追跡されない

// 制限③ 分割代入や、関数への受け渡しで、接続が切れる
const s = reactive({ count: 0 })
let { count } = s
count++                                // s には反映されない
callSomeFunction(s.count)              // ただの数値が渡るので、変化を追えない
  • オブジェクト型にしか使えない:オブジェクト、配列、MapやSetなどに限られ、文字列・数値・真偽値は持てません。
  • 丸ごと置き換えられない:Vueの追跡は、プロパティへのアクセスで行われるため、同じ参照を保つ必要があります。
  • 分割代入に弱い:プリミティブのプロパティを、ローカル変数に取り出したり、関数に渡したりすると、接続が切れます。

こうした制限があるため、公式は、リアクティブな状態の宣言には、ref()を主なAPIとして使うことを推奨しています。複数の値でも、まずはrefを1つずつ使うのが、迷わない方法です。

reactiveを分割代入したいときは toRefs

どうしてもreactiveを使い、分割代入もしたい場合は、toRefs()でrefのまとまりに変えます。作られたrefは、元のオブジェクトと同期します。単一のプロパティだけなら、toRef(state, 'foo')です。

JavaScript:toRefsで接続を保つ
import { reactive, toRefs } from 'vue'

function useFeature() {
  const state = reactive({ foo: 1, bar: 2 })

  // ... state を使った処理 ...

  // 返すときに toRefs で ref のまとまりにすると、分割代入しても接続が切れない
  return toRefs(state)
}

const { foo, bar } = useFeature()
foo.value++              // もとの state.foo も更新される

ライフサイクルフック

Options APIのmountedなどに対応する関数(onMountedなど)を、インポートして使います。setupの実行中に、同期的に呼ぶ必要があります。

Vue:ライフサイクルフック
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'

// ライフサイクルフックは、関数としてインポートして使う。
// setup の実行中に、同期的に呼んで登録する
onMounted(() => {
  console.log('マウントされました')
})

onBeforeUnmount(() => {
  console.log('破棄される直前です')
})
</script>

算出プロパティ・ウォッチャー・props

Composition APIでは、これらも関数として書きます。それぞれ、詳しい解説の記事があります。

Composableとして切り出す

Composition APIの強みは、状態とロジックを、関数にまとめて再利用できることです。こうした関数を「Composable」と呼び、名前はuseで始めます。refを返せば、受け取る側で分割代入しても、リアクティブのままです。

JavaScript:useCounter.js
// useCounter.js:状態とロジックを、関数にまとめる
import { ref } from 'vue'

export function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  return { count, increment }        // ref を返す(分割代入しても、リアクティブのまま)
}
Vue:Composableを使う
<script setup>
import { useCounter } from './useCounter'

const { count, increment } = useCounter()
</script>

より実用的な作り方はComposable関数でロジックを共通化する方法をご覧ください。

よくある質問(FAQ)

Qrefとreactiveは、どちらを使うべきですか?
A公式は、ref()を主なAPIとして使うことを推奨しています。reactiveは、オブジェクト型にしか使えず、丸ごと置き換えたり、分割代入したりすると、リアクティブな接続が切れるためです。
Qreactiveを分割代入したら、画面が更新されなくなりました。
Aプリミティブの値を取り出すと、ただの値になり、接続が切れます。toRefs()でrefのまとまりに変えてから分割代入するか、最初からrefを使ってください。
Q<script setup>とsetup()関数は、どう違いますか?
A<script setup>は、トップレベルの変数や関数を、自動でテンプレートに公開する書き方で、SFCでは一般的です。setup()関数は、テンプレートで使うものをreturnする必要があります。
Qテンプレートで{{ object.id + 1 }}が、「[object Object]1」になります。
Aテンプレートで自動で取り出されるのは、トップレベルのrefだけです。ネストしたrefは、const { id } = objectのように、トップレベルの変数に取り出してください。

まとめ

Composition APIは、<script setup>とrefを軸に、状態とロジックを機能ごとにまとめる書き方です。reactiveの3つの制限に注意し、迷ったらrefを使いましょう。共通のロジックは、Composableに切り出して、再利用してください。