Vue2.xで、composition-api + TypeScript使っているときのメモ
これはメモです。
Nuxt.js 2.14.x + TypeScript + @vue/composition-api 導入時につまずいたことや
忘れがちなこと、レビューで指摘された話題などを共有する場所です。
したがって、Vue2.x とはタイトルに示したものの、SSR特有の事情などを記載する場合があります。
template ref の挙動について
<template>
<div>
<my-component ref="my" />
<button @click="test">Test</button>
</div>
</template>
<script lang="ts">
import {defineComponent, ref} from '@vue/composition-api'
import MyComponent from '@/components/MyComponent.vue'
export default defineComponent({
components: {MyComponent},
setup() {
// ref<?> に、対象コンポーネントのインスタンス型を渡すことで、ref.value の
// 入力補完が効いて素敵。
// https://github.com/vuejs/composition-api/issues/402
const my = ref<InstanceType<typeof MyComponent>>()
const test = () => {
// MyComponent の something() を呼ぶことができるが、ref は undefined の可能性がある。
my.value?.something()
}
return {
// my には、template 上で、同じ名前の ref が自動設定される
// https://github.com/vuejs/composition-api/blob/cf5fa2bb8f37277f281f42d073a18ef6ae24c181/src/utils/instance.ts#L55
my,
test
}
}
})
</script>
にある通り、Vue.js 3.x にある、Function ref には対応していないので注意
props に対して分割代入を使うべからず
ドキュメントにはっきりと書いてあるが、setup で渡されてくる props を 分割代入してはいけない。
変更のトラッキングがかからなくなってしまうとのことだ。
❌
<template>
<div>{{foo}}</div>
</template>
<script lang="ts">
import {defineComponent} from '@vue/composition-api'
export default defineComponent({
props: {
foo: {
type: String,
required: true
}
},
setup({foo}) {
// foo を使う
}
})
</script>
⭕
<template>
<div>{{foo}}</div>
</template>
<script lang="ts">
import {defineComponent} from '@vue/composition-api'
export default defineComponent({
props: {
foo: {
type: String,
required: true
}
},
setup(props) {
// props.foo を使う
}
})
</script>
useAsync の利用上の注意
@nuxtjs/composition-api
にあるuseAsync は、setup() 時に呼ぶ関数で、SSR時はサーバサイドで情報を取得し、SPA時にはページ遷移時に呼び出したいものを用意できるという代物。
Options API ベースでいうと、 asyncData
でやっていたことを対応する場合に使うものだが、これを使うときには注意するべき点がある。
マニュアルには記載があるが
On the server, this helper will inline the result of the async call in your HTML and automatically inject them into your client code. Much like asyncData, it won't re-run these async calls client-side.
However, if the call hasn't been carried out on SSR (such as if you have navigated to the page after initial load), it returns a null ref that is filled with the result of the async call when it resolves.
とのことで、要約すると asyncData
のように、useAsync
はサーバサイドでロードされ、結果をHTMLに書き出す挙動になっている。このため、クライアントでは再実行されなくて済む。
ただし、useAsync で呼び出す関数の結果自体が null だと、client でも呼び出されてしまう。
例として、someFunction
という非同期で情報をとってくる関数を考えるとする。
以下の場合は、someFucntion() は、サーバサイドおよびクライアントサイドの両方でコールされる。
仮に内部でWebAPIなどにコールするような処理があるのであれば、1画面を1APIコールで済むのに、2APIコール (しかも同じ内容) になるのはよろしくない。
❌ サーバでも、クライアントでも someFunction() がコールされてしまう。
<template>
<div />
</template>
<script lang="ts">
import {defineComponent, useAsync} from '@nuxtjs/composition-api'
import {someFunction} from '~/lib/api'
export default defineComponent({
setup() {
useAsync(async () => {
await someFunction()
})
return {}
}
})
</script>
以下の場合は呼ばれずに済む
⭕ SSRおよび画面遷移したクライアントでのみ呼ばれる
<template>
<div />
</template>
<script lang="ts">
import {defineComponent, useAsync} from '@nuxtjs/composition-api'
import {someFunction} from '~/lib/api'
export default defineComponent({
setup() {
useAsync(async () => {
await someFunction()
return true
})
return {}
}
})
</script>
そもそも null
ではない返り値があるなら、それを受け取って使えば、Ref
が得られるので、それを使えば良い。
⭕ SSRおよび画面遷移したクライアントでのみ呼ばれる
<template>
<div>{{foo}}</div>
</template>
<script lang="ts">
import {defineComponent, useAsync} from '@nuxtjs/composition-api'
import {someFunction} from '~/lib/api'
export default defineComponent({
setup() {
const foo = useAsync(() => someFunction())
return {foo}
}
})
</script>
ところで、useAsync
とは別に、単純に非同期な関数をSSR時と画面遷移時に実行したいという目的のために、useFetch
が存在するが、これはこれで computed
と同時に利用しようとすると、エラーが発生してしまう。
非常にやっかみのある問題である。。解決としては現状は、、useFetch
を使いたい場合は computed
を使わないか、 useAsync
を使うということになりそうだ。