Nuxt 2.12で導入された fetch() ってどんなもの？

この投稿は [How fetch works in Nuxt 2.12](https://nuxtjs.org/blog/understanding-how-fetch-works-in-nuxt-2-12)　（2020/4/6寄稿）の翻訳版です。
![main.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7804/a4b31706-1a9b-3ae6-58d9-5992534edf2b.png)

Nuxt 2.12で `fetch` が利用できるようになりました。この `fetch` を利用することでNuxtアプリケーションへの新たなData連携が可能となります。
この記事では、 `fetch` が他のライフサイクルhookとどう違って、どう機能するのかを紹介していきます。

## FetchとNuxtライフサイクル

Nuxtライフサイクルhookにおいて、 fetchはcreatedの後に実行されます。Vueのライフサイクルhookではthisコンテキストが利用できるわけですが、このfetchでも同様です。

![new-fetch-lifecycle-hooks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7804/dccada5b-1467-4c03-d3e1-1732d9676b5f.png)

FetchはサーバーサイドでComponentインスタンスが作られた後に呼ばれるので、`this`コンテキストが`fetch`内で利用できるわけです。

```javascript
export default {
  fetch() {
    console.log(this);
  }
};
```

これがpageコンポーネントにおいてどういう意味を持つのかを見てみましょう。

### Pageコンポーネント

`this`コンテキストが利用できることから、fetchではコンポーネントデータを直接操作することができます。つまり、コンポーネントのローカルデータの取得にVuexのactionやmutationを利用しなくても良いということです。

結果として、Vuexの利用は任意となりますが、利用は可能であるということになります。必要であれば、 `this.$store` を使ってVuex storeにアクセスすることは可能です。

## fetchの利用範囲

fetchを利用することで事前にdataを非同期取得できますが、これは、`/pages` 以下のpageコンオポーネントに限らず、`/layouts`や `/components`配下の**あらゆる.vueコンポーネント**でfetchの恩恵を受けることができることを意味します。

### Layoutコンポーネント

新たに導入された `fetch` を利用することでlayoutコンポーネントから直接APIを呼び出すことができるようになりました。これはv2.12より前のバージョンではできなかったことです。

利用例：

- バックエンドからconfigデータを取得し、footerやナビゲーションを動的に生成する
- ナビゲーションに表示するユーザ関連情報の取得（例：ユーザープロフィール、ショッピングカートの品物数）
- `layouts/error.vue` で利用するサイトに関連するデータ

### Building-blockコンポーネント（子コンポーネント）

子コンポーネントでも`fetch`利用が可能だということは、pageレベルでのデータ取得処理を子コンポーネントに委譲することができるということです。これもv2.12より前のバージョンではできなかったことです。

これにより、ルーティングレベル(route-level)のコンポーネントの負荷を大きく軽減することができます。

利用例：
- これまで通りpropsを子コンポーネントに渡すこともできますが、子コンポーネントに独自のデータ取得ロジックが必要であればそれも可能となります。

## 複数fetchの実行順

コンポーネントごとにデータfetchロジックを持つことになるので、その実行順については気になるところです。

Fetchはサーバーサイドで一度（Nuxtアプリケーションへの最初のリクエスト時に）呼ばれ、ルーティング処理に応じた呼び出しがクライアントサイドで行われます。しかしながら、各コンポーネントでfetchを実行できるため、これらfetchは階層順で呼び出されることになります。

### サーバーサイドfetchの無効化

サーバーサイドのfetch処理を無効化することもできます。

```javascript
export default {
  fetchOnServer: false
};
```

こうすることで、fetchはクライアントでのみ呼ばれることになります。この `fetchOnServer` がfalseに設定されると、コンポーネントがサーバーサイドでレンダリングされる際に、`$fetchState.pending` が `true` に設定されます。

## エラーハンドリング

fetchにおいては、エラーハンドリングはコンポーネントレベルで実行されます。

`fetch` では、データが非同期でデータが取得されるため、そのリクエストの完了および成功状態を確認するために `$fetchState` オブジェクトが提供されています。

```javascript
$fetchState = {
  pending: true | false,
  error: null | {},
  timestamp: Integer
};
```

keyが3つ存在します。

1. **Pending** - クライアントサイドでfetchが呼び出された際のplaceholder表示
2. **Error** - エラーメッセージの表示
3. **Timestamp** - 最後にfetchが呼び出されたtimestamp（keep-aliveを利用したキャッシュで有用）

これらのkeyはコンポーネントのtemplate内で、APIデータ取得中のplaceholderの表示に利用されます。

```javascript
<template>
  <div>
    <p v-if="$fetchState.pending">Fetching posts...</p>
    <p v-else-if="$fetchState.error">Error while fetching posts</p>
    <ul v-else>
      …
    </ul>
  </div>
</template>
```

## メソッドとしての利用

ユーザアクションやコンポーネントメソッドに応じたメソッドとしての利用も可能です。

```html
<!-- from template in template  -->
<button @click="$fetch">Refresh Data</button>
```

```javascript
// from component methods in script section
export default {
  methods: {
    refresh() {
      this.$fetch();
    }
  }
};
```

## パフォーマンスを意識したアプローチ

fetch利用おけるNuxt pageコンポーネントのパフォーマンスを良くするため、 `:keep-alive-props` propや `activated` hookの利用があります。

Nuxtではある一定のpageをデータと一緒にメモリーキャッシュできる仕組みがあります。また、データの再取得までの時間を設定することも可能です。

これを有効にするためには、一般的な `<nuxt />` や `<nuxt-child />` コンポーネントで `keep-alive` propを利用します。

```vuejs
<!-- layouts/default.vue -->
<template>
  <div>
    <nuxt keep-alive />
  </div>
</template>
```

加えて、 `<nuxt />` コンポーネントに `:keep-alive-props` を使ってキャッシュするページ数を渡します。

`:keep-alive-props` propを設定することで、サイト回遊中にメモリーキャッシュする最大ページ数を指定することができます。

```vuejs
<!-- layouts/default.vue -->
<nuxt keep-alive :keep-alive-props="{ max: 10 }" />
```

上記は一般的なパフォーマンス改善施策の一例ですが、以下はもっと踏み込んだ例で、`$fetchState.timestamp` を利用して、データの再取得までの時間を設定しています。

ここでは、 `kee-alive` と Vueの　`activated` hookを利用します。

```vuejs
export default {
  activated() {
    // Call fetch again if last fetch more than a minute ago
    if (this.$fetchState.timestamp <= Date.now() - 60000) {
      this.$fetch();
    }
  }
};
```

## asyncData vs Fetch

pageコンポーネントに関して言えば、 ローカルデータを扱うという点で、`fetch` は `asyncData()` と大変似ています。しかし、特筆すべき違いもいくつかあります。

Nuxt 2.12時点で、 `asyncData` はまだ利用可能です。では、これらの違いを見ていきましょう。

### AsyncData

1. `asyncData` はpageレベルのコンポーネントでのみ利用可能
1. `this` は利用不可
1. dataを**返す**ことでpayloadを渡している

```javascript
export default {
  async asyncData(context) {
    const data = await context.$axios.$get(
      `https://jsonplaceholder.typicode.com/todos`
    );
    // `todos` does not have to be declared in data()
    return { todos: data.Item };
    // `todos` is merged with local data
  }
};
```

### Fetch

1. `fetch` はすべてのVueコンポーネントで利用可能
1. `this` が利用可能
1. 単純にローカルデータの編集(mutate)をしている

```javascript
export default {
  data() {
    return {
      todos: []
    };
  },
  async fetch() {
    const { data } = await axios.get(
      `https://jsonplaceholder.typicode.com/todos`
    );
    // `todos` has to be declared in data()
    this.todos = data;
  }
};
```

## Nuxt 2.11以前のfetch

Nuxtに長らく関わっているのであれば、旧のバージョンの `fetch` はかなり違うことに気づくと思います。

**これはそんなに大きな変化なのでしょうか**

答えはNoです。古い `fetch` は依然として第1引数に `context` を渡すことができ、既存のNuxtアプリケーションの互換性を維持することができます。

以下に新旧での違いをまとめます。

### Point 1

**Before** - `fetch` hook はコンポーネントの初期化前に呼ばれ、 `this` は利用できない
**After** - `fetch` は、routeアクセス時、サーバーサイドでのコンポーネントインスタンス生成後に呼ばれる

### Point 2

**Before** - Nuxt contextにはpageレベルコンポーネントでアクセスし、そのcontextは第1引数として渡される

```javascript
export default {
  fetch(context) {
    // …
  }
};
```

**After** - 引数なしにVueクライアントサイド同様に `this` contextへのアクセスができる

```javascript
export default {
  fetch() {
    console.log(this);
  }
};
```

### Point 3

**Before** - Pageレベルコンポーネント（reoute-level）のみがサーバーサイドデータfetchができる

**After** - Vueコンポーネントで非同期のpre-fetchができる

### Point 4

**Before** - `fetch` はサーバーサイドでは1度（Nuxtアプリへの初回リクエスト時）、クライアントサイドでは、ナビゲーション発生時に呼び出すことができる

**After** - `fetch` は旧バージョンと同じだが...

...コンポーネント単位で `fetch` できるため、fetch hookはコンポーネント階層順に呼び出される

### Point 5

**Before** - API呼び出し時のエラーハンドリングとして、`context.error` functionを利用してページリダイレクトを実行していた

**After** - 新しい `fetch` では、template内で `$fetchState` オブジェクトを利用したエラーハンドリングが可能

**ということは、旧バージョンで可能だった、任意のエラーページへの誘導ができないということ？？**

そうなりまが、 pageレベルコンポーネントの場合 `asyncData()` を利用できます。シンプルに `this.$nuxt.error({ statusCode: 404, message: Data not found' })`　を利用してエラーページへリダイレクトしてください。

## 最後に

新しい `fetch` hookの導入により多くの機能改善や、データfetchおよびrouteレベル、子レベルコンポーネント設計における柔軟性がもたらされました。

これにより、同一routeにおける複数API呼び出しが必要なNuxtプロジェクトの設計においてすこし違う考え方を求められるでしょう。

この記事が新しい `fetch` 機能の理解の一助となること願っています。