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.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' }) } ...