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

この投稿は How fetch works in Nuxt 2.12 (2020/4/6寄稿)の翻訳版です。
main.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

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

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処理を無効化することもできます。

export default {
  fetchOnServer: false
};

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

エラーハンドリング

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

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

$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の表示に利用されます。

<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>

メソッドとしての利用

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

<!-- from template in template  -->
<button @click="$fetch">Refresh Data</button>
// 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を利用します。

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

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

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

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

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

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

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コンポーネントに関して言えば、 ローカルデータを扱うという点で、fetchasyncData() と大変似ています。しかし、特筆すべき違いもいくつかあります。

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

AsyncData

  1. asyncData はpageレベルのコンポーネントでのみ利用可能
  2. this は利用不可
  3. dataを返すことでpayloadを渡している
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コンポーネントで利用可能
  2. this が利用可能
  3. 単純にローカルデータの編集(mutate)をしている
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引数として渡される

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

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

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 機能の理解の一助となること願っています。