Vue 3 Composition APIでasync-awaitを使う方法

Vue.js

Vue 3から導入されたComposition APIでasync-awaitを使う方法をまとめました。
async-await自体の使い方は、こちらの記事を参考にしてみてください。

Vue 3 Composition APIでasync-awaitの基本的な使い方

まずはComposition APIの中でどのようにしてasync-awaitを記述するのか基本のかたちをみてみましょう。

ディレクトリ構造


root
├─ data
│   └─ db.json
├─ src
   ├─ App.vue
   ├─ components
   │   ├─ PostList.vue
   │   └─ SinglePost.vue
   ├─ views
   │   └─ Home.vue

db.json

{
  "posts": [
    {
      "id": 1,
      "title": "タイトル1",
      "body": "Lorem ipsum",
      "tags": ["web開発", "コーディング", "news"]
    },
    {
      "id": 2,
      "title": "タイトル2",
      "body": "Lorem ipsum",
      "tags": ["css", "web開発", "コーディング"]
    }
  ]
}

Home.vue

<template>
  <div class="home">
    <h1>Home</h1>
    <div v-if="error">{{ error }}</div>
    <div v-if="posts.length">
    <PostList :posts="posts" />
    </div>
    <div v-else>Loading...</div>
  </div>
</template>

template内で、ref関数で指定したerrorを表示したい場合は、error.valueとする必要はなく、errorとします。
postsがあったら表示したいときは、v-ifでlengthを条件とすることで、0のときfalse、それ以外のときはtrueとなります。

<script>
import PostList from '../components/PostList.vue'
import { ref } from 'vue'

export default {
  name: 'Home',
  components: { PostList },
  setup() {
    const posts = ref([])
    const error = ref(null)

    const load = async () => {
      try {
        let data = await fetch('http://localhost:3000/posts')
        console.log(data); //statusが OKか確認する。

        if(!data.ok) { //okというプロパティがありtrue/falseで返す
          throw Error('No data available')
        }
        posts.value = await data.json()
      } catch (err) {
        error.value = err.message
        console.log(error.value);
      }
    }

    load()

    return { posts, error }
  }
}
</script>

上のようにsetup()の中でasync-awaitを通常のJavaScritpと同じように使うことができます。

ref関数を使うので、vueからimportします。
リアクティブにするため、postsもerrorもref関数で宣言して、returnで返します。

postsの値にアクセスするときは、ref関数なのでvalueをつけます。

変数loadを忘れずに実行しましょう。

async-awaitをコンポーネント化する

上の記述方法では、async-awaitの部分を使うたびに記述する必要があるので、関数にして切り出すと便利になります。

ディレクトリ構造

関数として使うために、srcの中にcomposablesというフォルダを作成し、JavaScriptファイルを作成します。


root
├─ data
│   └─ db.json
├─ src
   ├─ App.vue
   ├─ components
   │   ├─ PostList.vue
   │   └─ SinglePost.vue
   ├─ composables
   │   ├─ getPosts.js
   ├─ views
   │   └─ Create.vue
   │   └─ Home.vue

Home.vue

<script>

import PostList from '../components/PostList.vue'
import getPosts from '../composables/getPosts'

export default {
  name: 'Home',
  components: { PostList },
  setup() {
    //getPosts()はreturnした{posts, error, load}を返す
    // なので、分割代入してそれぞれを受け取る
    const { posts, error, load } = getPosts() 

    load()
    
    return { posts, error }
  }
}
</script>

setup()の中身をload()実行前の部分まで切り取って、作成したJavaScriptファイルにペーストします。
今回はgetPosts.jsとしています。

まずは、getPosts.jsをimportします。
そして、getPosts()でそれぞれを返すようにしておき、それらを分割代入して変数に格納することでHome.vueでも使えるようになります。

getPosts.js

import { ref } from 'vue'

const getPosts = () => {
  const posts = ref([])
  const error = ref(null)

  const load = async () => {
    try {
      let data = await fetch('http://localhost:3000/posts')
      console.log(data) //statusが OKか確認する。

      if (!data.ok) {
        //okというプロパティがありtrue/falseで返す
        throw Error('No data available')
      }
      posts.value = await data.json()
    } catch (err) {
      error.value = err.message
      console.log(error.value)
    }
  }

  // 使えるようにreturnする必要がある
  return {posts, error, load}
  // load()はHome.vueから呼び出したい
  // load()
}


export default getPosts

まず、ref関数を使えるようにするために、vueからimportする文をHome.vueからカットしてきます。
そして、Home.vueで記述していたasync-awaitの部分を関数の中にペーストします。
Home.vueや他のファイルでも使えるように、変数と関数をreturnします。

最後に忘れずにexportすることで、他のファイルでこの関数を使うことができるようになります。

ローディングの確認をするとき

ローディング時のスピナーなどの動作を確認したい場合、擬似的にローディング時間を長くする方法がいくつかあります。
最も簡単な方法の一つとして、次の記述をasync-awaitのtryブロックに入れる方法があるので紹介します。

  const load = async () => {
    try {
      // simulate delay
      await new Promise(resolve => {
        setTimeout(resolve, 2000)
      })
...
    } catch (err) {
...

上は2秒ですが、時間を調整してローディング時の動作を確かめることができます。

jsonデータをPOSTで送信する

フォームに入力した内容を、jsonデータに変換して、jsonファイルに追加できるようにします。
Create.vueを作成します。
そして、index.jsに追記します。

index.js

...
import Create from '../views/Create.vue'

...
  {
    path: '/create',
    name: 'Create',
    component: Create,
  }
...

Create.vue

<template>
  <div class="create">
    <form @submit.prevent="handleSubmit">
      <label>タイトル</label>
      <input v-model="title" type="text" required />
      <label>内容: </label>
      <textarea v-model="body" required></textarea>
      <label>タグ (エンターキーでタグを追加)</label>
      <input v-model="tag" type="text" @keydown.enter.prevent="handleKeydown" />
      <div v-for="tag in tags" :key="tag" >#{{ tag }}</div>
      <button>記事を投稿</button>
    </form>
  </div>
</template>

@submitを設定して、フォームの内容をjsonデータに送信できるようにします。

<script>

import { ref } from 'vue'
export default {
  setup() {
    const title = ref('')
    const body = ref('')
    const tag = ref('')
    const tags = ref([])

    const handleKeydown = () => {
      // tagを一つにしたいので、tagsにtagの値がないときにという条件にする
      if (!tags.value.includes(tag.value)) {
        // 不要な空白を削除しておきたい
        tag.value = tag.value.replace(/\s/, '') //空白を取り除く
        tags.value.push(tag.value) //そしてtagsにtagを追加する
      }
      tag.value = '' //tagをクリアしておく
    }

    const handleSubmit = async () => {
      const post = {
        title: title.value,
        body: body.value,
        tags: tags.value
      }
      await fetch('http://localhost:3000/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(post)
      })
    }

    return { title, body, tag, handleKeydown, tags, handleSubmit }
  }
}
</script>

handleSubmitでは、async-awaitを使って非同期処理します。
jsonデータのそれぞれの項目と入力されるデータを紐付けたオブジェクトを変数postに格納します。
そして、fetchではawaitをつけて非同期にして、第二引数を設定します。

ただ、上の状態だと投稿が完了した後もjsonデータには追加されていますが、その後のrouterの設定がないのでページがトップに戻ったりしません。

Composition APIでRouterを使う

Composition APIではsetup()を使いますが、setup()ではthisがきかないのでthis.$routerは使えません。
そのため、routerを使うときは、useRouterをimportします。


import { useRouter } from 'vue-router'

そして、routerなどの変数に格納することで使えるようになります。


const router = useRouter()

そして、上のCreate.vueの場合では、たとえばトップページに戻るようにしたいのなら、
fetchが完了した後、routerをpush()します。

...
    const handleSubmit = async () => {
      const post = {
        title: title.value,
        body: body.value,
        tags: tags.value
      }
      await fetch('http://localhost:3000/posts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(post)
      })
      router.push({ name: 'Home' })
    }
...

Vue.js

Posted by devsakaso