くちたと計算記

プログラミングのことを書きます

Nuxt3 の useFetch / useAsyncData で POST リクエストを送るときはリクエストがキャッシュされないように注意しよう

概要

useFetch で key を設定せずに POST リクエストを送信する実装をしてみたら、 ボタンを何回押しても一度しかリクエストが送信されなくなってしまいました。$fetch を直接使ったり、 key を適切に設定すれば解決しました。

前提知識

  • TypeScript
  • Nuxt 3

起きた問題

ログインフォームの submit イベントに、以下のようなイベントハンドラを登録しました。

const username = ref("")
const password = ref("")

const onSubmit = async () => {
  const formData = new FormData();
  formData.set("username", username.value);
  formData.set("password", password.value);
  const { error } = await useFetch("/api/login", {
    method: "POST",
    body: formData
  });
}

ところが、一度でも間違ったアカウント情報でログインを試行すると、正しいアカウント情報を入力しなおしてもログインできないという事象が発生しました。

原因

開発者ツールでネットワークを監視してみると、何度ログインを試行しても HTTP リクエストが一度しか送信されていないことが分かりました。これは、 useFetch と useAsyncData が HTTP リクエストをキャッシュしているからです。 useFetch は options で指定した key、 useAsyncData は引数で指定した key をキャッシュのキーに使っています。では明示的に key を指定しなかったときに何が key として使われるのかを確認してみます。

useFetch

以下のように、URL と オプションを使って自動的に key を生成すると書いてあります。

key: a unique key to ensure that data fetching can be properly de-duplicated across requests, if not provided, it will be automatically generated based on URL and fetch options

useFetch · Nuxt Composables

具体的にどのオプションを使っているのかは、以下のソースコードを見ればわかります。

const _key = opts.key || hash([autoKey, unref(opts.method as MaybeRef<string | undefined> | undefined)?.toUpperCase() || 'GET', unref(opts.baseURL), typeof _request.value === 'string' ? _request.value : '', unref(opts.params || opts.query), unref(opts.headers)])

nuxt/packages/nuxt/src/app/composables/fetch.ts at 38b6d88cfab3477edabfa37d6f97b95eefc6ae6c · nuxt/nuxt · GitHub

ソースコードによると、以下の文字列をもとに key を生成しているようです。

  • URL
  • HTTP メソッド
  • クエリパラメータ
  • HTTP ヘッダ

useAsyncData

key: a unique key to ensure that data fetching can be properly de-duplicated across requests. If you do not provide a key, then a key that is unique to the file name and line number of the instance of useAsyncData will be generated for you.

useAsyncData · Nuxt Composables

公式リファレンスによると、ファイル名と行番号ごとにユニークな値を key として生成しているようです。

対策

以下のいずれかの対策をすれば良さそうです。

  1. POST リクエストを送信するときには、直接 $fetch を使う。
  2. HTTP リクエストをキャッシュしてほしくないときには、 key を明示的に指定する。

POST リクエストを送信するときには、直接 $fetch を使う。

リクエストをキャッシュしているのは useFetch と useAsyncData の仕様なので、それらを使わないという考え方です。 useFetch のキャッシュの条件を見るに、これらの Composable は GET リクエスト用なのかなと考えてしまいました。

HTTP リクエストをキャッシュしてほしくないときには、 key を明示的に指定する。

以下のように、 key がリクエスト毎に変わるように設定すれば良さそうです。タイムスタンプでなくても、 uuid を使ってもいいですね。

const username = ref("")
const password = ref("")

const onSubmit = async () => {
  const formData = new FormData();
  formData.set("username", username.value);
  formData.set("password", password.value);
  const { error } = await useFetch("/api/login", {
    method: "POST",
    body: formData,
    { key: `/api/login?${new Date()}` }
  });
}

GET リクエストの場合は、どの key でリクエストをキャッシュしてほしいか考えたうえで、 key を設定したほうが良さそうです。

おまけ

基本的にどのリクエストもキャッシュしてほしくないという場合には、 useFetch のラッパーを作ってもいいかもしれません。

公式のリファレンスに載っている実装例をもとに、 デフォルトの key を設定する Composable を書いてみました。 Use Custom Fetch Composable · Nuxt Examples

export function useCustomFetch<T> (url: string | (() => string), options: UseFetchOptions<T> = {}) {

  const defaults: UseFetchOptions<T> = {
    key: `${url}?${new Date()}`,
  }
  const params = defu(options, defaults)
  return useFetch(url, params)
}

参考 Web ページ