Vue 3 Composition APIでasync-awaitを使う方法
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.vueHome.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' }) } ...
