Open4

Vue2.xで、composition-api + TypeScript使っているときのメモ

ピン留めされたアイテム
ooharabucyouooharabucyou

これはメモです。

Nuxt.js 2.14.x + TypeScript + @vue/composition-api 導入時につまずいたことや
忘れがちなこと、レビューで指摘された話題などを共有する場所です。
したがって、Vue2.x とはタイトルに示したものの、SSR特有の事情などを記載する場合があります。

ooharabucyouooharabucyou

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>

https://github.com/vuejs/composition-api#template-refs
にある通り、Vue.js 3.x にある、Function ref には対応していないので注意

ooharabucyouooharabucyou

props に対して分割代入を使うべからず

https://v3.vuejs.org/guide/composition-api-setup.html#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>
ooharabucyouooharabucyou

useAsync の利用上の注意

@nuxtjs/composition-api にあるuseAsync は、setup() 時に呼ぶ関数で、SSR時はサーバサイドで情報を取得し、SPA時にはページ遷移時に呼び出したいものを用意できるという代物。
Options API ベースでいうと、 asyncData でやっていたことを対応する場合に使うものだが、これを使うときには注意するべき点がある。

https://composition-api.nuxtjs.org/helpers/useasync/

マニュアルには記載があるが

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 と同時に利用しようとすると、エラーが発生してしまう。
https://polidog.jp/2021/03/27/nuxt_composition_api/

非常にやっかみのある問題である。。解決としては現状は、、useFetch を使いたい場合は computed を使わないか、 useAsync を使うということになりそうだ。