RareJob Tech Blog

レアジョブのエンジニア・デザイナーによる技術ブログです

NuxtでAPI RequestをComposition Functionにする

APP/UXチームに所属しております、フロントエンドエンジニアの田原です。

夏が終わり、最近すっかり肌寒くなってきましたが皆さん如何お過ごしでしょうか?
最近、私の周りで結婚や婚約する友人・知人がとても多く、おめでたい出来事に嬉しくなる反面、個人的にはとても心寒い日々が続いております。

話は変わり、Vueを扱うフロントエンドエンジニアの皆さんにとっては既知の出来事かと思いますが 先日、Vue3が正式にリリースされたので界隈では大いに賑わっておりました。

github.com 名称がワン●ースなので、世はまさに大Vue時代、「Vueのアーキテクチャーか?欲しけりゃくれてやる。探せ!この世のすべてを3.0系に置いてきた!」 的なノリなのかな?とか勝手に感じておりますw

このリリースによりVue自体に新しく追加された機能は幾つかありますが、Composition APIを正式に使えるようになったということを嬉しく思っている方が多いのかなと思っています。(私もその一人です)

弊社でもこのリリースを見越してComposition APIをプロダクト内で利用できるようにしていきたいね!ということだったので 実験的な部分もありますがNuxtベースに作成しているプロダクト内でComposition APIを使えるようにしております。 参考:Nuxt Composition API

今回はNuxt+Composition APIを使い、Ajax Request(axios-moduleを利用)をComposition Functionとして関数を切り離した 実装をご紹介できればと思っております。まだまだ手探り状態である為、一つの方法として見て頂けると嬉しいです。 もっとこうしたら良いよ!などの優しい指摘・意見を頂けると嬉しいです。

目次

Composition Apiとは

自身の理解を大まかに一言で示すと 「ロジックの再利用性の向上とロジックとComponentの依存性を低くできる」新しい仕組みと捉えております。

こちら→公式Composition APIに詳細な説明がありますし、 様々な方が利点や旨みについて説明されているのでこのAPIの基本的な使い方や 意義についてなどの説明については、末尾参考の各記事をご覧ください。

useApi.tsの作成

まずはベースとなる、API Requestを行うFunctionの作成を行います。 ※ファイル名、変数名については適当なものを設定しているのでご容赦を

※尚、useFetchはNuxt Composition APIの機能名として既に存在する為、ご注意ください。 今回はuseApi.tsという名称でベースを作成します。

import { reactive, toRefs } from '@nuxtjs/composition-api'
import { NuxtAxiosInstance } from '@nuxtjs/axios'

// 各型は参考まで
type Options = {
  headers: {
    'X-transaction-ID'?: string
    'x-api-key'?: string
    Authorization: string
    'Content-Type'?: string
  }
}
type Params = {
  [key: string]: any
}
type baseState = {
  response: {}
  otherError: Error | null
  isLoading: boolean
}

const useApi = (
  $axios: NuxtAxiosInstance,
  url: string,
  params?: Params,
  options?: Options
) => {
  const state = reactive<baseState>({
    response: {},
    otherError: null,
    isLoading: false
  })
  // GET
  const getData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$get(url, options)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  // POST
  const postData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$post(url, params, options)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  // PUT
  const putData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$put(url, params, options)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  // DELETE
  const deleteData = async () => {
    state.isLoading = true
    try {
      const res = await $axios.$delete(url, params)
      state.response = res
    } catch (error) {
      state.otherError = error
    } finally {
      state.isLoading = false
    }
  }
  return { ...toRefs(state), getData, postData, putData, deleteData }
}
  • @nuxtjs/composition-apiをimportします
  • axios-moduleの型をimportします。
  • useApiという名称で関数を定義して、response値やLoadingステータスなどのstate値をreactiveにします。
  • CRUDの関数を用意します。(ここではtry/catch/finallyを利用しておりますがNuxt内でaxios-moduleを使った一般的なRequestで問題ないです。)
  • returnでstateと各関数を返却します。この時stateはtoRefsでreturnしてuseApi.tsを利用する関数内で個別のreactiveな値として利用できるようにしておきます。

useSampleApi.tsの作成

次にuseApi.tsを呼び出す各endpoint側のComposition Functionを作成します。
ここではuseSampleApi.tsとしています。

import { toRefs, reactive } from '@nuxtjs/composition-api'
import { NuxtAxiosInstance } from '@nuxtjs/axios'
import useApi from '~/composition/useApi'

const useSampleApi = (axios: NuxtAxiosInstance) => {
  const sampleState = reactive<{
    response: any
    error: Error | null
    isLoading: boolean
  }>({
    response: [],
    error: null,
    isLoading: false
  })
  const apiGetTrigger = async () => {
    const { response, otherError, isLoading, getData } = useApi(
      axios,
      `https://hogehogehoge.com`
    )
    sampleState.isLoading = isLoading as any
    await getData()
    sampleState.response = response as any
    sampleState.error = otherError as any
  }
  return { ...toRefs(sampleState), apiGetTrigger }
}
  • こちらもuseApi.ts同様に@nuxtjs/composition-apiとaxios-moduleの型をimportします。
  • useApi.tsから取得できる値を詰め直すstateをreactiveにします。
  • useApi.ts内で設定したXXXData関数(ここではgetData)を利用する関数を用意します。(apiGetTrigger)
  • apiGetTrigger内でaxiosとpathを渡します。(後述しますがaxiosはcomponentから関数利用時に渡します)
  • useApi.tsから返されたreactiveな値をuseSampleで設定したreactiveなstateに代入します。 (この時、toRefsとreactiveの型が異なる為、error回避のためanyでキャストしています。イケてないので良い方法があれば知りたいです。)
  • 関数とreactiveな値をreturnで返却します。

※awaitでgetData()を待つ理由として、api request後にresponseかotherErrorに値が返るのでそうしています。 isLoadigは表示側で常にreactiveな値として変化できるように、最初に代入しております。

Component内での利用

最後にComponent側でuseSampleApi.tsから取得できる各値を利用していきます。

<template>
  <div>
    <h2>Sample</h2>
    <section>
      <form @submit.prevent="apiGetTrigger">
        <button>Getlist</button>
      </form>
    </section>
    <div v-if="otherError">
      <h2>otherError !! {{ otherError }}</h2>
    </div>
    <div v-if="isLoading"><h2>Fetching Data</h2></div>
    <div v-for="list in response" :key="list.id">
      <ul>
        <li>
          <span>{{ list.name }}</span>
          <span>{{ list.street }}</span>
          <span>{{ list.city }}</span>
          <span>{{ list.postal_code }}</span>
        </li>
      </ul>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
import useSampleApi from '~/composition/useSampleApi'
export default defineComponent({
  setup(_props, {root}) {
    const { $axios } = root
    const {
      response
      isLoading,
      apiGetTrigger,
      otherError
    } = useSampleApi($axios)

    apiGetTrigger() // こんな感じでsetup内で呼び出すとイベントではなくLifecycle内で呼ぶことも可能です。

    return {
      isLoading,
      response,
      apiGetTrigger,
      otherError
    }
  }
})
  • ComponentをdefineComponetを使って作成
  • setup関数内でrootを指定してpluginとしてinjectされている$axios(axios-module)を取り出す
  • useSampleApi.tsをimportして、setup関数内で各値を取り出す
  • template内で利用したい値をreturnで返す

※apiGetTriggerについてはコメントにある通り、setup関数内で呼び出せばComponentのLifecycle内で呼ぶことも可能です。

まとめ

useXXXの形式のAPI Requestを行う処理を関数単位で外に切り出すことができることで、Component内では返り値を扱うだけで 欲しい情報をtemplate部分に反映させることができる為、Component内にロジックが乗らないので見通しが良くなります。 また、別のComponentにimportして利用すれば同様のRequest処理を行える為、再利用性なども高いと思います。 関数ベースにJestをかければ良いのでJest.mockを利用した疑似API Requestのテストについても簡便に書くことができるので 便利だなぁと恩恵を受けております。 (Jestについては今度、書きます。) まだまだ、できることの把握と理解が追いついてないので手探り状態ではありますが参考になれば嬉しいです。

参考