🏖️

Vue3のCompositionAPIとうまく向き合う方法

2022/11/30に公開

CompositionAPIうまく使えていますか?

Vue3が出て早いもので2年が経ちました。
Nuxt初のVue3対応版であるNuxt3.0もようやくstableでリリースされました。
https://nuxt.com/v3
自分は仕事で今年Vue3に上げたばかりなのですが、自分なりのCompositionAPIに対するTipsを書いていきたいと思います。

CompositionAPIは何を解決したのか

OptionsAPIは論理的関心事(ロジック)が散らばる

OptionsAPIとは2系で一般的だったdata,method,computedなどVueの提供する機能に即した場所にコードを書いていくAPI、記法です。
これにより、リアクティブに扱いたいデータはdataに、自動で再計算されてキャッシュされて欲しい、もしくは変更の可能性のないデータはcomputedに、さまざまな処理はmethodにという風にまとめて書くことができます。

export default {
  // 内部で使いたいコンポーネント
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  // 親からもらうデータ
  props: {
    user: {
      type: String,
      required: true
    }
  },
  // リアクティブに扱いたいデータ
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  // 自動で再計算されて欲しい値
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  // 変更を検知して呼び出したいメソッド
  watch: {
    user: 'getUserRepositories' // 1
  },
  // 処理
  methods: {
    getUserRepositories () {
      // `this.user` を使用してユーザーのリポジトリを取得します
    }, // 1
    updateFilters () { ... }, // 3
  },
  // ライフサイクルフック
  mounted () {
    this.getUserRepositories() // 1
  }
}

このAPIは「Vueの機能ごとにまとめて書ける」という点においては便利でしたが、一つのコンポーネントにいろんなロジックが入り混じると見通しが悪くなります。

あなたは新しいプロジェクトに参加しました。
そのプロジェクトはそこそこ大規模で、OptionsAPIで書かれています。
あなたは初めてのタスクを任されました。
新しく管理者以外のロールが追加されたことに伴って、ユーザー情報詳細ページにおいて

  • 管理者以外のユーザーは他の人のユーザー情報を閲覧はできるが、変更はできないようにする
  • ただし、管理者も他の管理者の変更は出来ないようにする
    というものです。
    幸い、管理者かどうかなどの情報はすでにAPIから返ってきています。
    更新を防ぐだけなら関数にif文を追加すれば済みそうです。

あなたはユーザー情報詳細ページを見てみました。
現在のユーザーデータ更新のロジックの他に、今は関係ないロジックが以下のように同じコンポーネントに入っています。コードはなぜかscriptタグだけで1000行ほどあり、そこそこ見応えがあります。
あなたはユーザーデータ更新のロジックを変更する時に、今回変更する予定ではないロジックが含まれた、本来見なくても良いのコードの山から自分が変更したいロジックのコードを何度も探すことになりました。

UserDetail.vue
export default {
  data () {
    return {
        isLoading: false,
      company: null,
      mode: null,
      ...

      // 本来見ればいいのはここと
      user: null,
    }
  },
  computed: {
    // ここと
    canUpdate: () => { this.user && this.user.role !== 'admin' }
    ...
  },
  methods: {
    handleHoge() {},
    ...
    // ここだけのはず
    updateUser() {},
  },
  async created() {
    const company = await this.fetchCompany()
    ...
  }
}

あなたは、「コードがdataとそれを使う処理、つまりロジック毎に綺麗に分かれていればなぁ」と思いました。
それがCompositionAPIです。

CompositionAPIで論理的関心事をまとめる

この実装をCompositionAPIに落とし込むとこうなります。

function useUser() {
  const user: User | null = reactive(null)
  ...

  const updateUser = (id: number, data: FormData) => {
    if(loginUser.role !== 'admin' || user.role === 'admin') {
      throw new Error('Invalid Operation')
    }
    return axios.patch(`/users${id}`, data)
  }

  return {
    updateUser,
    ...
  }
}

const { updateUser } = useUser()
updateUser(id, data)

useUserというcomposableに「user」とそれを使う「updateUser」をまとめました。
つまり、データとその処理が一つの関数にまとまったのです。
このロジックの単位に分けられた関数をcomposableと呼びます。(ReactでいうHooksです。)

CompositionAPIのTips

dataとそのdataを使う処理でまとめてcomposableを切る

上記のようにフレームワークの機能に合わせて書くのではなく、ロジックにまとめて切る。

dataをreadonlyにしてsetterを定義する

2024/4/23追記:これよりはdataではなくcomputedで返した方がいい

これはオブジェクト指向からのtipsですが、composableからの戻り値は変更可能です。

例えば、roleをadmin権限でしかいじれないようにしたい場合このようにcomposableを書くと思います。

function useUser() {
  const changeRole = (role: Role) => {
    if(loginUser.role !== 'admin') throw new Error('Invalid Role')
    user.role = role
  }

  return {
    user,
    changeRole,
  }
}

const { user, changeRole } = useUser()
const handleChangeRole = (value: Role) => {
  changeRole(value)
}

しかし、userが返されている場合これは変更可能なのでchangeRole関数を無視してこのように書けてしまいます。

const { user, changeRole } = useUser()
const handleChangeRole = (value: Role) => {
  // changeRoleを使用せずに変更できてしまう
  user.role = value
}

これを防ぐためにはreadonlyをつけましょう。

function useUser() {
  ...
  return {
    readonly(user),
  }
}

const { user, changeRole } = useUser()
const handleChangeRole = (value: Role) => {
  // 警告が出る
  user.role = value
}

これで変更できなくなります。
オブジェクト指向のカプセル化みたいなもんですね。

TypeScriptを必ず入れる

ここで断言しますが、Vue3(CompositionAPI)からTypeScriptは必須です。
なぜなら変数をリアクティブにするAPI、refを使用すると変数の実態に「.value」経由でアクセスしなければならないからです。
またtemplateタグ内では一層目だけ.valueが勝手にアンラップされるなど落とし穴が多いです。
これはプログラマーが変数のリアクティブを管理する必要があることを意味します。

const user = reactive({ name: 'yodaka' })
user.name = 'yodaka'

const userName = ref('yodaka')
userName.value = 'yodaka'
const nested = { userName }
<template>
  <div>{ user.name }</div>
  <div>{ userName }</div>
  <div>{ nested.userName.value }</div>
</template>

これをプログラマーがいちいち覚えているのはしんどいです。
型に助けてもらいましょう。
Nuxtでプロジェクトをセットアップすれば、確か自動でTypeScriptが入るようになっているはずです。

終わりに

ちょっと眠いので何かミスってたらすみません。。
CompositionAPIはロジックをまとめられる反面、Vueの機能ごとにまとめる書き方ではなくなります。
それはVue、OptionsAPIに慣れていた人たちからしたら確かに違和感があるかもしれませんが、プログラミングって本来データがあってその下にそれを使うメソッドがあって〜と関連するまとまりは近くに置くべきものですよね。
なので今回はCompositionAPIはそういったプログラミングの原則に立ち返るものでもあるかなと感じています。
CompositionAPIのTips自分も欲しいのでみなさんも是非記事を投稿してください!

Discussion