2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

useAsyncDataでSSRではないアプリのAPIクライアントキャッシュ運用を始めました

Posted at

はじめに

Nuxtには useAsyncData というfetch系のComposablesが存在します。
ユーザー別のページやニュース一覧など、動的ルーティングのページ間を頻繁に行き来するようなサイトにおいて都度fetchするのはオーバーヘッドが大きいです。
そのため、データの性質を見つつクライアント側でAPIレスポンスをキャッシュことが多いです。

useAsyncDataは上記のようなユースケースでシンプルな管理が可能になります。

useAsyncData provides access to data that resolves asynchronously in an SSR-friendly composable.

SSRフレンドリーと記載がありますがもちろんCSRでも使えます。

使い方

基本的な使い方は以下に記載されています。
https://nuxt.com/docs/4.x/api/composables/use-async-data

抜粋

<script setup lang="ts">
const { data, status, pending, error, refresh, clear } = await useAsyncData(
  'mountains',
  (_nuxtApp, { signal }) => $fetch('https://api.nuxtjs.dev/mountains', { signal }),
)
</script>

useAsyncDataの引数は以下で構成されています。

第1:キャッシュキー。fetchされたデータはこのキーと紐づいてキャッシュされます。
第2:非同期処理のハンドラー。ここで取得したいデータのAPI呼び出し処理を定義します。
第3:オプション。`watch`や`getCachedData`、`immediate`等の設定を定義し、
     データ取得タイミングやキャッシュ利用の制御ができます。

オプションで使う項目

default

データがfetchされるまでのデフォルト値を定義するコールバック。
lazy: trueimmediate:false 等を設定している際に予期せぬ画面表示を防ぐことができます。

transform

ハンドラーによって取得されたデータを加工したり変更することができる関数。
getCachedDataと併用する際には注意が必要(外部のcomputedに出した方が良いかも)

getCachedData

キャッシュデータを取得するコールバックを書きます。
キャッシュはキーと紐付けされ、 nuxtApp.payload.data に格納されます。
この返却値がundefined,nullだった場合は非同期ハンドラーを実行してデータをfetchします。

<script setup lang="ts">
    // 以前fetchしたデータとキーの組み合わせがあればfetchせずにキャッシュを返却
    getCachedData: (key, nuxtApp) => {
        return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key]
        //NOTE: return値がundefine | nullだった場合はfetch実行
    }
</script>

watch

変数を監視し、変更があればハンドラーを再実行できます。

NOTE: ここで第1引数のキーを指定してしまうと、キャッシュがあろうが再fetchを実行してしまうため注意が必要です。
副次的変数の変更をトリガーに再計算したい場合に使うのがオススメです。

Case1. 動的ルーティング時のAPI呼び出し

Twitterの投稿URLを想像してください。
https://x.com/ubugeeei/status/1997543798654800210

上記のようなパスは動的ルーティングで構成されています。

https://x.com/<user_id>/status/<post_id>

こういった動的パスで設計される画面ではroute paramsを用いてAPIを呼び出すことが多い。
上記ポストではGraphQLを用いてツイートの詳細情報を取得している。

Xはともかく、実際に運用するプロダクトで以下のようなケースはuseAsyncDataの恩恵を受けられる。

  1. 動的ルーティングのパスを行き来することが多い
  2. 取得するデータは頻繁に更新されない(具体的には秒・分単位)

実装例

<script setup lang="ts">
    const route = useRoute()
    
    const userIdComp = computed(()=>(route.params?.user_id) ?? '' as string)
    const postIdComp = computed(()=>(route.params?.post_id) ?? '' as string)
    
    const postEndpoint = computed(()=> `/users/${userIdComp.value}/posts/${postIdComp.value}`)
    
    const { data } = await useAsyncData(postEndPoint,
        ()=> fetch(postEndpoint,{method: "POST"}),
        {
            getCachedData: (key, nuxtApp) => {
                return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key]
            }
        }
    )
    
</script>

パスパラメータをcomputedで監視し、変更が加わったタイミングでuseAsyncDataが再計算され、

  1. getCachedDataでキャッシュの有無をチェック
  2. なければfetchを実行してデータ返却

という流れで処理してくれます。

Case2. 画像のHEADキャッシュ

レアなケースですが、動的出力する画像パスに対し「画像が存在するか」という点をHEADメソッドで確認します。
GETでも判定可能ですが、判定時点ではバイナリは不要なのでHEADで取得します。

実装例

<template>
    <div>
        <img v-if="=enableImage" :src="imagePath"/>
        <p v-else>画像がありません</p>
    </div>
</template>
<script setup lang="ts">
    const route = useRoute()
    
    const userIdComp = computed(()=>(route.params?.user_id) ?? '' as string)
    const imageIdComp = computed(()=>(route.params?.image_id) ?? '' as string)
    
    const imagePath = computed(()=> `/users/${userIdComp.value}/images/${imageIdComp.value}`)
    
    const { data: enableImage } = await useAsyncData(imagePath,
        async() => await fetch(imagePath, {method: "HEAD"})
            .then((res)=>{
                // MEMO: HEADで404だとしてもthenに入るケースがあるためstatusでチェック
                if(res.status === 200) return true
                return false
            }).catch(()=>{
                return false
            }),
        {
            getCachedData: (key, nuxtApp) => {
                return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key]
            }
        }
    )
</script>

useAsyncDataをstoreに定義すれば動的ページを跨いでもリロードしたり外部ページを経由するまではキャッシュを再利用できるため、都度HEAD処理を待機せずに済み、通信量も減るのでオーバーヘッドが抑えられます。

useFetchとの違い

以下のcoedoさんの記事にて詳細に解説されています。

ラッパーや使い方の違いはありますが、私が特に気になったのは型の付け方です。

  type CustomResponse = {
    id: string
    name: string
  }
  
  const { data } = await useAsyncData<CustomResponse>('post', async () => {
    return { id: '1', name: 'sample' }
  })

  const { data: data2 } = await useFetch<CustomResponse>('/')

どちらもdataへ型付けすることは可能ですが、useAsyncDataは第2引数のハンドラを書く際に型ガードしたり、async/awaitの付け外しが必要だったりで少し癖があります。

シンプルに通信するだけで良いのであればuseFetchを使う方が書きやすそうです。

使った感想

これまでAPIのクライアントキャッシュ制御はフルスクラッチしていましたが、このようなcomposableがあるとグッと楽になりますね。
非同期ハンドラーはNuxtの機能に依存していないため、repositoriesとして外部に持たせることができるのでNuxtから剥がしやすいのも利点だと思います。

AbortSignalによる制御や手動でのrefresh,clearなど、まだ使いきれていない機能がたくさんあるためこれからも使い込んでいこうと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?