Developersをフォローする

Vue3 + compositioiAPIを使ってみた

フロントエンド

業務でvue2 + optionsAPIをvue3 + compositionAPIに移行する機会があったので、以下のことについて自分なりにまとめてみました。

  • optionsAPIとcompositionAPIの違い
  • compositionAPIに移行したほうが良いパターン、しなくてもよいパターン

optionsAPIとcompositionAPIの違い

optionsAPI

コンポーネントのオプション (data, computed, methods, watch) ごとにロジックをまとめる。

compositionAPI

機能ごとにコンポーネントオプションをまとめることができる。

書き方の比較

同様のコンポーネントで、optionsAPIとcompositionAPIの書き方を比較してみます。

以下の2つの機能があるコンポーネントを考えてみます。

  1. 適当な外部 API からユーザー名に対応したリポジトリを取得して、ユーザーが変化するたびにそれを更新する
  2. searchQuery 文字列を使用してリポジトリを検索する

optionsAPIの場合

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      searchQuery: '' // 2
    }
  },
  computed: {
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // `this.user` を使用してユーザーのリポジトリを取得します
    }, // 1
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}

見ての通り、1,2,3それぞれの機能がdata(),computed(),watch()といったコンポーネントオプションに点在しているため。
コード行数が増えるごとに可読性が落ちてしまいます。

上記の欠点を解消したのがcompositionAPIになります。

compositionAPIの場合

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch, toRefs, computed } from 'vue'

// コンポーネント内部
setup (props) {
  // props の `user` プロパティへのリアクティブな参照を作成するために `toRefs` を使用します
  const { user } = toRefs(props)

  // 1
  const repositories = ref([])
  const getUserRepositories = async () => {
    // リアクティブな値にアクセスするために `props.user` を `user.value` に更新します
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)

  // ユーザープロパティへのリアクティブな参照のウォッチャをセットします
  watch(user, getUserRepositories)

  //2
  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(
      repository => repository.name.includes(searchQuery.value)
    )
  })

  return {
    repositories,
    getUserRepositories,
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}

具体的な書き方はさて置き、1, 2の機能をひとつのまとまりとして書けるようになり読みやすくなりました。

compositionAPIの代表的な書き方について見ていきましょう。

  1. コンポーネントオプション (data, computed, methods, watch)はsetup()内部に記載し、末尾のreturn{}に登録することでtemplateで使用可能になる。
  2. ライフサイクルフック(mounted,destroyedなど)もsetup()に登録できる。
     ※ただし、setupはcreated()と同じタイミングで実行されるため、created()は廃止されている。
  3. reactiveな変数はref([])で定義する。

2, 3についての細かな説明は省きますが、ここでは1について比較してみます。

// optionsAPI
  methods: {
    getUserRepositories () {
      // `this.user` を使用してユーザーのリポジトリを取得します
    },
  },
// compositionAPI
setup (props) {
   const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(user.value)
  }
  return {
  	getUserRepositories
  }
}
 // template
   <button class="btn btn-success" @click="getUserRepositories">ユーザー取得</button>

compostionAPIの場合は、getUserRepositoriesをmethods()内部にくくらずにsetup()内部に定義し、return{}に記載することでtemplateでそのままgetUserRepositoriesを使用することができます。

このようにコンポーネント内部での機能ごとの分離はできましたが、すべてをsetup()内部に書くとコードが肥大化してしまう問題があります。
そのため、1, 2の機能ごとにそれぞれファイルに切り出してみます。

// src/composables/useUserRepositories.js ※1の機能

import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'

export default function useUserRepositories(user) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(user.value)
  }

  onMounted(getUserRepositories)
  watch(user, getUserRepositories)

  return {
    repositories,
    getUserRepositories
  }
}
// src/composables/useRepositoryNameSearch.js ※2の機能

import { ref, computed } from 'vue'

export default function useRepositoryNameSearch(repositories) {
  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => {
      return repository.name.includes(searchQuery.value)
    })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}

機能1を useUserRepositories
機能2を useRepositoryNameSearch
としてjsファイルで定義しました。
これらのコンポーネントでreturn{}したコンポーネントオプションは、は先ほどのコンポーネントのsetup()内部でインポートしてそのまま使用することができます。

// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import { toRefs } from 'vue'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup (props) {
    const { user } = toRefs(props)

   // 機能1をimport
    const { repositories, getUserRepositories } = useUserRepositories(user)

   //機能2をimport
    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    return {
      repositories: repositoriesMatchingSearchQuery,
      getUserRepositories,
      searchQuery,
    }
  }
  data(){
   ////
  }
  methods(){
   ////
  }
}

setup()には外部ファイルからimportした機能を定義して、以降のコンポーネントオプションでコンポーネント独自の機能を定義することでより見通しがよくなる気がしますね

このように、簡単に機能ごとに分離できるのがcompositionAPIの利点といえます。

compositionAPIを生かせるパターン、そうでもないパターン

compositionAPIの利点はわかりましたが、実際にoptionsAPIから移行するには機能を理解・分割する労力とセンスが必要だと感じたので
実際に過去に携わった案件で方針を考えてみました。

compositionAPIを生かせるパターン

  • 複数コンポーネントで同一のリアクティブな値を参照するとき

例えば複数のタブで同じrepという値を参照し、リアクティブに更新したい場合はよりシンプルに書けます。

  • 単一コンポーネントで複数の機能を使用する場合

単純にコード書く量が減るのでうれしいです。

そうでもないパターン

  • コンポーネント間が単純な親子関係のとき

propsのやりとりで足りる気がします。

  • optionsAPIのvuexから移行するとき

vuexのmapGetter等は、compositionAPIには対応していないようなので、全面書き直しになります。可読性は上がりますが、作業量的に大変だなと思いました。。