Hiratake Web ロゴ

NuxtのuseAsyncDataのキーを安易にroute.pathにしてはいけない

投稿した日
更新した日
書いたひと
icon
ひらたけ

Vue.js のフレームワークである Nuxt には、データを取得する場合などに利用する useAsyncData というコンポーザブルがあります。このコンポーザブルを使用する際、第 1 引数に一意の key を指定しなければならないのですが、この key の値を安易に route.path としてはいけない という話です。

useAsyncData の key とは

Nuxt で Nuxt Content モジュールや microCMS のようなヘッドレス CMS を使用してブログサイトを構築するとき、pages ディレクトリに配置した .vue ファイルから画面に表示する記事データを取得する必要があります。その場合には useAsyncData コンポーザブルを使用してデータを取得することになると思います。

Nuxt Content のドキュメント でも、以下のように useAsyncData でデータを取得する例が示されています。

<script setup>
const { data } = await useAsyncData('home', () => queryContent('/').findOne())
</script>

<template>
  <pre>
    {{ data }}
  </pre>
</template>

useAsyncData コンポーザブルを使用する場合には第 1 引数に一意の key を指定する必要があり、上の例では home が key になります。

key は重複してはいけない

この key ですが、今年の初めにリリースされた Nuxt v3.10.0 にて experimental.sharedPrerenderData というオプションが導入され、これを有効にしている場合には 重複している useAsyncData の呼び出しは排除され値がキャッシュされる とあります。

Your useAsyncData and useFetch calls will be deduplicated and cached between renders of your site.

home という key が指定された useAsyncData が複数回呼び出される場合、1 回目を実行したときに第 2 引数に指定した関数の結果が保存され、2 回目以降は保存したデータを使うようになります。

このオプションは、以下ドキュメントによると現在開発が進められている Nuxt v4 リリース時にはデフォルトで有効となる ようなので、今のうちからデータの取得時には key の値が重複しないように実装しておくべきと考えられます。

これで困るのは pages/[...slug] など、各ブログ記事の内容を表示するファイルでのデータ取得で、key に article のような文字列を指定してしまうと 全ての記事が同じ内容になってしまう ことになります。実際に experimental.sharedPrerenderData を有効にしてビルドを実行してみると、全ての記事が同じ内容になっているのが確認できました。

そこで、この投稿のタイトルにもある key に route.path を指定するという話が出てくるわけです。

const route = useRoute()
const { data } = await useAsyncData(
  route.path,
  () => queryContent(route.path).findOne()
)

ブログの記事データを取得するケースを考えると、それぞれの ページのパス(URL)を key とすれば重複することなく一意の値として与えることができる はずです。Nuxt を使用して構築されている UnJS のウェブサイトのリポジトリ などでも、key に route.path が指定されていました。

route.path を指定した場合の問題

key に route.path を指定することで、全ての記事が同じ内容になってしまう問題は防ぐことができます。基本的にはこれで問題ないと思われるのですが、たとえば 記事データとは別にそのページに関連する別のデータも取得したいというような場合に問題が発生する ことがあります。

たとえば、記事データとは別にその記事の前後の記事のデータを取得するケース。前後の記事はそれぞれのページごとに異なるデータが返ってくることになるので、${route.path}_surroundroute.path を含めるような形で key を指定するとします。

const route = useRoute()
const { data: surround, error: surroundError } = await useAsyncData(
  `${route.path}_surround`,
  () => {
    // データを取得する処理
  },
)

route.path の key は記事データの取得で使用しているため、route.path の後ろに別の文字列をくっつけた文字列を指定して一意の値となるようにしています。

このような実装をしたウェブサイトを公開してしばらく経った頃に、検索エンジンでお馴染みの Google 社が提供している Google Search Console というツールでウェブサイトの情報を確認してみると 大量の「見つかりませんでした(404)」となっているページが発生 しました。

Google Search Console の「見つかりませんでした(404)」の画像

useAsyncData の key に指定した値は、ページ内にリンクが貼られるわけではありませんがビルド後のソースコード内に出力されています。それを検索エンジンのクローラーが拾ってしまい、ページが見つからないというエラーが発生してしまっているようでした。

ウェブサイトの表示に影響があるというわけではありませんが、ひたすら存在しないページのエラーが出続けるというのはあまり気分の良いものではありません。

URL と判定されない key を使用する

おそらく key の文字列の先頭が / で始まっているために URL と解釈されてしまっている、という状況だと思うので、surround_${route.path} のように route.path を最初に持ってこないようにすれば解消されるのではないかと思います。

私は以下のような関数を用意して、/- に変換した key を生成できるようにして useAsyncData コンポーザブルに指定するようにしました。

/**
 * パスからuseAsyncDataで使用するキーを生成する
 * @param path useAsyncDataで取得するコンテンツのパス
 * @param name 指定したパスに関連して取得するデータの一意な名前
 * @returns useAsyncDataで使用するキー
 */
export const pathToUseAsyncDataKey = (
  path: string,
  ...name: string[]
): string => {
  const generatedPath = path
    .split('/')
    .filter((item) => item)
    .join('-')
  const generatedName = name.map((item) => `_${item}`).join('')
  return `${generatedPath}${generatedName}`
}

先ほどの画像でも分かる通り、key の値を変えてからしばらくすると徐々に「見つかりませんでした」のエラーは減少。減るまでに半年以上かかりましたが、これで無駄なエラーが発生することにモヤモヤしなくて済みそうです。


画面上にエラーが出るとかではないですが、存在しないページへ検索エンジンのクローラーからのアクセスが発生するのもあまり良いものではないので、気をつけようという話でした。