Nuxt3 Fetchの使い方

先週Nuxt3が正式リリースされました。
RCから色々と使っていたのですがAPIにリクエストを飛ばすuseFetchがデフォルトでキャッシュしなくなった(デフォルトでキャッシュしている問題を解決するためのinitialCacheが消えた）りと便利になりました。

そこでNuxt3では結局useFetchどう使えばいいの？というコードをまとめてみました。

---

- バージョン情報
    - Nuxt: 3.0.0

---

## Fatchコード

```typescript
# 通信先のAPIは後述するカウントを返すJSONを受け取る { value: 0 }

const getCounter = async (): Promise<number> => {
  const URL = `/api/counter`
  const METHOD = 'GET'
  const { data, pending, refresh, error } = await useFetch(URL, {
    key: `${uuid.v4()}`, // POINT1
    method: METHOD,
    // credentials: 'include', //POINT3
    // headers: HEADERS, //POINT4
    // params: params, // POINT5
    async onResponse({ request, response, options }) {
      console.log('onResponse log1', request, response.status)
      for (const header of response.headers.entries()) {
        console.log('onResponse HEADER', header)
      }
    }, // POINT2
  })
  console.debug(URL, data.value, error)
  return data.value?.value ?? 0
}

```

- POINT部分を説明していきます

## POINT1 keyについて

このkeyはオプショナルな設定属性なのですが、これがないと**同じリクエストを並列・非同期に実行すると取得される結果が同じになる**。という問題がおきます。

```text
※ 
同時に同じリクエスト投げるケースがない人は書かなくてOKです。
自分の場合、コンポーネント化した中でAPIをcallしており、
呼び出し側の状態によって同じリクエストパラメタになるケースがあったため、この設定をしています。
```

keyについてはドキュメントを覗いてみると

[https://nuxt.com/docs/api/composables/use-fetch#usefetch](https://nuxt.com/docs/api/composables/use-fetch#usefetch)

```
リクエスト間でデータの取得が重複しないようにするための一位なキー。
指定がない場合、静的コードの場所（クラスや行数）に基づいてキーが生成される。
```
とあります。

実験するため先程のfetchの通信先APIを作ります。
nuxt3にはserverフォルダを掘るとlocalなapiが作れるので以下のようなカウントアップするapiを用意

```typescript
# server/api/counter.get.ts
class CountState {
  value = 0
  getCounter() {
    return this.value
  }
  setCounter(value: number) {
    this.value = value
  }
  addCounter() {
    this.value++
  }
}
const counter = new CountState()

export default defineEventHandler((event) => {
  counter.addCounter()
  return {
    value: counter.getCounter(),
  }
})
```

これを以下のように呼び出す

```vuejs
<template>
  <div>
    <div>
      <p>counter1: {{ countView1 }}</p>
      <p>counter2: {{ countView2 }}</p>
      <button @click="callApi">CountUP</button>
    </div>
  </div>
</template>
<script lang="ts" setup>
const callApi = async () => {
  //ここで非同期に同時実行する
  const results = await Promise.all([getCounter(), getCounter()])
  countView1.value = results[0]
  console.log('count1', countView1.value)
  countView2.value = results[1]
  console.log('count2', countView2.value)
}

const countView1 = ref(0)
const countView2 = ref(0)

const getCounter = async (): Promise<number> => {
  const URL = `/api/counter`
  const METHOD = 'GET'
  const { data, pending, refresh, error } = await useFetch(URL, {
    // key: `${uuid.v4()}`, KEYを使ってないケース
    method: METHOD,
    async onResponse({ request, response, options }) {
      console.log('[fetch response]', request, response.status)
      for (const header of response.headers.entries()) {
        console.log('HEADER', header)
      }
    },
  })
  console.debug(URL, data.value, error)
  return data.value?.value ?? 0
}

onMounted(async () => {
  callApi()
})
</script>
```

### 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240634/405eb72a-5130-d220-20b8-40394c98605f.png)

これはドキュメントの説明通り同じURLを呼び出したためです。

この状態にならない方法が以下３つになります
- keyを一位なコードで指定する（さきほどのコードのkey属性のコメントを外す
- 並列ではなく直列で呼ぶ
- パラメタが異なる(GET/POSTでもパラメタが違えば大丈夫）

```typescript
# awaitして連続で呼ぶ
const callApi = async () => {
  countView1.value = await getCounter()
  console.log('count1', countView1.value)
  countView2.value = await getCounter()
  console.log('count2', countView2.value)
}

# パラメタを変える
const getCounter = async (value: number): Promise<number> => {
  const URL = `/api/counter`
  const METHOD = 'GET'
  const { data, pending, refresh, error } = await useFetch(URL, {
    method: METHOD,
    params: {
      id: value,
    },
    async onResponse({ request, response, options }) {
      console.log('[fetch response]', request, response.status)
      for (const header of response.headers.entries()) {
        console.log('HEADER', header)
      }
    },
  })
  console.debug(URL, data.value, error)
  return data.value?.value ?? 0
}

## 呼び出し側
const results = await Promise.all([getCounter(1), getCounter(2)])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240634/09d16a77-8405-2358-3eb8-e3faf59c483b.png)


まったく同じURLに２回リクエストすることがないよね？と思いますがAPIをcallしているコンポーネントを作り、それを複数同時に呼び出す用に設定すると困るケースがある。

結果がキャッシュされて、２回のリクエスト１回分にしているのかと思いきや普通にリクエストは２回飛んでいました。
しかし片方の結果のみになる。(最初に書いたRC時代にあったinitialCacheは1回のリクエストしか飛ばさず同じ結果が取れてた）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240634/45a5e5fa-0108-7fe4-b372-df4c8866791b.png)

というわけで、keyを指定しましょう。というPOINTになります。

## POINT2 onResponse

onResponseではレスポンスのステータスコードやヘッダーが取得できます

デフォルトのレスポンスはuseFetchのdata, pending, refresh, execute, errorでは取れない？みたいなので、onResponseでレスポンスの中身を取得しました。

## POINT3 credentials

認証のセッションクッキーを利用する場合に利用。
自分が試したのはLaravelのSanctum

## POINT4 Header

ヘッダー情報を指定したい場合に利用

## POINT5 パラメタ

- GETの場合はparams, POST等の場合はbodyというキー名でobjectで渡せばOK



というわけでNuxt3のFetchの使い方の覚書になります。
POINT1でハマったのでちょっと多めです