🍩

Nuxt 3 でも useSWR したい!→標準機能のuseFetchでできました

2022/09/08に公開
6

React には、データ取得・状態管理・キャッシュなどを少ない記述で実装できる SWR というライブラリがあります。

https://zenn.dev/mast1ff/articles/5b48a87242f9f0

この記事を読んで、Nuxt 3 で同じことをする場合、どのようなライブラリを使うのが良いのか調べてみました。

TL;DR

ライブラリは不要で、Nuxt 3 の標準機能で実装可能です。

useFetch() または useAsyncData() のオプションで initialCache: false を指定するだけでした。

(11/16追記)正式リリース前の最終RCである v3.0.0-rc.14 で、initialCache オプションが削除されました。つまり、デフォルトで自動的に今回の記事で紹介する挙動になりました。

https://github.com/nuxt/framework/pull/8885

以下は、そもそもSWRとは何か、そしてuseFetch / useAsyncData とは何か、という説明なので、上記の説明で理解できた方は読まなくて大丈夫です。

SWR について

useSWR とは

swr とは、Next.js と同じ Vercel が提供している React 向けのライブラリです。

https://swr.vercel.app/ja

https://zenn.dev/yukikoma/articles/17adad7fedd5af

stale-while-revalidate というキャッシュ戦略に基づいており、「APIには毎回リクエストを送りつつ、結果が返ってくるまでの間だけ古いキャッシュを利用することで、UI上は高速なレスポンスを維持しながら、できるだけ最新の状態を反映する」という仕組みになっています。

このスマートなキャッシュ管理に加え、pending や error といった取得の状態もあらかじめ用意されているため、ローディングやエラーハンドリングなどを自分でいちいち定義して書く必要がなくなります。

さらに、APIからのデータ取得と返却されたデータのリアクティブ化まで一体化されているため、1つのフックでデータ取得に関する全ての面倒を見てくれます。

import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json());

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

データ取得の前後でローディングのフラグを自分で切り替えたり、エラーキャッチを書いたり……といった手間から解放され、しかもキャッシュ管理まで良い感じにしてくれる素晴らしいライブラリです。

Vue における useSWR

https://tech.hey.jp/entry/2020/09/29/115822

こちらの記事で説明されている通り、Vue 向けには SWR を基にした SWRV というライブラリが提供されています。

ここでは SWRV そのものの説明はしませんが、この記事の最後にこんな記述がありました。

実は SWRV はかなり前から存在していて、時期的には大体 Composition API のブリッジライブラリが出た頃だったと記憶しています。ではなぜ今さら来たと表現するのかというと、Nuxt 3 にて SWR ライクなデータフェッチの仕組みを提供すると発表されたためです。

SWRV は Nuxt のコアメンバーがメンテナンスしていることもありこのあたりに関連性を見出せそうです。直接利用されるかどうかはわかりませんが、今のうちにその概念自体は学んでおくべきと言えます。

この記事が書かれたのは Nuxt 3 の RC どころかパブリックベータも公開されていなかった 2年前。果たして Nuxt 3 の SWR はどうなったのでしょうか。

Nuxt 3 における Data Fetching

結論から言えば、Nuxt 3 のデータフェッチング機構である useFetch useAsyncData に、ほぼ同様の仕組みが搭載されています。

しかし、そもそも useFetchとは何か、useFetch と useAsyncData がどう違うのかという理解がないと、どのような仕組みなのかわからないと思うので、まずはその説明を先にします。(既に知っている方は飛ばしてください)

Nuxt 3 の提供するデータフェッチ関数

https://v3.nuxtjs.org/guide/features/data-fetching/

Nuxt 3 では、データ取得・管理のために useFetchuseAsyncData という2つの関数が提供されています。

useFetch という名前だけ見ると単に API にリクエストするだけの、 axios の延長みたいな機能にも見えますが、そうではなく、どちらもデータ管理・ローディング/エラーのハンドリングなど、useSWR に相当する機能が一通り搭載されていると考えてもらって良いです。

ちなみに、Nuxt 2 の Options API で asyncDatafetch というものがあったと思いますが、あまり理解の助けにはならないので忘れてください

あと、厳密には useLazyFetchuseLazyAsyncData というものもありますが、それぞれ初回のデータ取得のタイミングを任意に変える(マウント時に自動で取得しない)だけの違いなので、ここでは割愛します。

useAsyncData

まず、useAsyncData は、第1引数で key、第2引数で fetcher関数、第3関数で各種オプションを渡します。

実際の使い方を見た方が早いと思うので、公式ドキュメント(https://v3.nuxtjs.org/api/composables/use-async-data )のコード例を引用します。

const { data, pending, error, refresh } = await useAsyncData(
  'mountains',
  () => $fetch('https://api.nuxtjs.dev/mountains')
)

返却されている data, pending, error, refresh という値については何となくイメージがつくでしょう。データ、データ取得中かどうか、エラー内容、再取得のトリガー関数、です。これが返ってくる時点で useSWR っぽい動きができるのが感覚的にわかるのではないでしょうか。

第1引数がアプリケーション中で一意なキーです。例えば、複数のコンポーネントで同じURLからのデータを取得する際に、key に同じ文字列を指定すると、リクエストが1度しか飛びません。

第2引数に渡されているのが、useSWR と同じように実際のデータを取りに行くための fetcher 関数 ということになります。($fetch は後述します)

第3引数の Options には文字通り様々なオプションを渡すことができます。 lazyオプション、サーバーサイドでリクエストするかどうか、デフォルト値などがあります。

const { data, pending, error, refresh } = await useAsyncData(
  'mountains',
  () => $fetch('https://api.nuxtjs.dev/mountains'),
  { lazy: true, default: [] }
)

その他、transform 関数で受け取ったデータを加工して返すこともできるので、これまでは computed で行っていたようなデータの整形も1つの変数で実現できます。

useFetch

一方の useFetch は何かというと、useAsyncData をさらに使いやすくしたヘルパー関数です。

const { data, pending, error, refresh } = await useFetch('https://api.nuxtjs.dev/mountains')

これだけで、useAsyncData とほぼ同じ挙動が得られます。

useAsyncData の 第1引数 に渡していた key は必須ではなくなりました。

key: a unique key to ensure that data fetching can be properly de-duplicated across requests, if not provided, it will be generated based on the static code location where useAsyncData is used.

未検証ですが、おそらく key には ファイルと行数が自動で使われる という挙動になっていると思われます。これだけだと同じURLでも別々のキーになってしまうので、useAsyncData とは違ってuseFetch では options に key というパラメータが追加されており、ここで文字列を自分で指定することもできます。

const { data, pending, error, refresh } = await useFetch('https://api.nuxtjs.dev/mountains', { key: 'mountains' })

fetcher関数もなくなり、URLを渡すだけで良くなりました。このURLに $fetch でリクエストされ、結果を取得します。

さらに、methods、params、headersといった、$fetch に渡したいオプションも第2引数の Optionsで渡すことができます。

自動のURL補完と型推論

ちなみに余談ですが、useFetch および $fetch では、存在する API Routes をサジェストしてくれる機能があり、API Routes の結果の型も自動で検出してくれます。API Routes に存在しない URL を指定すると unknown 型になります。

グローバルインポートに抵抗がある方も、ここまで補完が利いていれば便利に使えるのではないでしょうか。

つまり何が違うのか

何が言いたいかというと、useFetch も useAsyncData も仕組みとしてはほぼ同じものだということです。

そもそもuseFetch が useAsyncData のシュガーシンタックスなので、返ってくる値も data, error, pending などで共通しているし、オプションもベースは同じだということです。

つまり、この後で swr の説明をしますが、それは useFetch でも useAsyncData でも全く同じように動くし、useLazyFetch でも useLazyAsyncData でも変わりません。

$fetch (ohmyfetch) について

ところで $fetch が何なのかというと、これもグローバルインポートされているヘルパー関数です。

$fetch は、Nuxtチームによって開発されたohmyfetch という Fetch API ライブラリを簡単に呼び出すための関数で、自動インポートされているのでどこからでも呼び出せます。(型推論もされます)

Nuxt 3 では axios ではなく標準搭載されたこちらのAPIを使うことが推奨されており、この $fetch を使うメリットとして、SSR で Nuxt 3 内部の API Routes にアクセスする際に内部アクセスで処理されるという記述があります。

https://v3.nuxtjs.org/api/utils/$fetch/

ブラウザネイティブである fetch() や、これまでのスタンダードである axios にしかできないこともあると思いますので、どちらが優れているかについては言及を避けますが、$fetch を使いたくない場合は useAsyncData を選び、こだわりがない場合は useFetch を選ぶと良いのではないでしょうか。

Nuxt 3 で useSWR するには

さて、やっと useSWR の話に戻ってきました。

useFetch の initialCache オプション

useFetchuseAsyncData も、デフォルトでデータがキャッシュされ、同じkeyに対して一度リクエストしていたら、それ以降は再検証を行いません

これはページ遷移などで別のコンポーネントがマウントされた場合も、共通のkeyであればAPIリクエストは行わず、キャッシュがそのまま使われます。明示的に refresh を呼ばれた場合のみ再検証が行われます。(数時間ページを開いたままでも更新されないのかなどは未検証です)

しかしこれでは SWR の「常に最新の状態が反映される」は実現できず、リロードするまで新しい結果は反映されません。

ここで、useFetch の第2引数、useAsyncData の第3引数である Options の中に、 initialCache というものがあります。デフォルト値は true です。

これを false にすると、ページ遷移などでコンポーネントが再度読み込まれた場合に、必ずAPIへのリクエストが行われます。

しかし、だからといって data がすぐにリセットされるわけではなく、APIリクエストが完了するまでは前回の取得結果をキャッシュとして表示し、データ取得が完了したタイミングで表示内容が更新されます。

これはまさに SWR の特性であるスマートなキャッシュ管理であり、これができるなら「Nuxt 3 は SWR に対応している」と言って良いはずです。

Nuxt 3 で SWR の実装例

ということで、冒頭に貼った useSWR のサンプルコード。

import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((res) => res.json());

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

これを Nuxt 3 で書き換えるとこうなります。

<script lang="ts" setup>
const { data, error } = useFetch("/api/user", { initialCache: false });
</script>

<template>
  <div v-if="error">failed to load</div>
  <div v-else-if="!data">loading...</div>
  <div v-else>hello {{ data }}!</div>
</template>

<script setup> 構文の恩恵と、useFetch が Auto Import されている効果もあって、外部ライブラリを使うことなく React + useSWR と同等かそれ以上の簡潔さで記述することができました。

また、Vue 3 は JSX/TSXもサポートしているので、TSXで書くこともできます。

export default defineComponent({
  setup() {
    const { data, error } = $(
      useFetch("/api/user", { key: "/api/user", initialCache: false })
    );
    return () => {
      if (error) return <div>failed to load</div>;
      if (!data) return <div>loading...</div>;
      return <div>hello {data.name}!</div>;
    };
  },
});

TSX では key に明示的な文字列を渡さないとエラーになってしまいましたが、それ以外は Vue Template と同様で、JSX部分は完全に React と同じ記述で実装できました。

useFetch を使っている以上は関数型コンポーネントではなくステートを持つため、 defineComponent を使うことになります。(script setup と JSX を組み合わせることは現状では基本的にできません)

defineComponent では、新たに導入された defineProps などの便利な記法が使えず、記述量が長くなりがちなので、個人的には Vue Template に強い抵抗がなければ <script setup> をオススメしたいところです。

なお、JSX部分を React に合わせるために、 .value アクセスを避けられる Reactivity Transform を採用していますが、これは2022/9/8現在ではまだ Experimental 扱いなので、nuxt.config で有効化が必要です。

https://v3.nuxtjs.org/api/configuration/nuxt.config#reactivitytransform

まとめ

Vue / Nuxt で useSWR みたいなことをしたいと思って調べてみたのですが、標準搭載された機能だけで実装できるということに気づいて驚きました。

useSWR の利点である定期的なポーリングやフォーカス復帰といった細かい動作、useSWRInfinite のような関連ライブラリと同じ動きに対応しているかまではまだ調べられていないのですが、少なくともデータ管理やエラーハンドリングを簡潔に書きたいという、メインの用途は十分に満たしてくれそうです。

皆さんもぜひ Nuxt 3 の useFetch / useAsyncData を有効活用してみてください!

余談

initialCache オプションに関する公式ドキュメントの説明文は以下です。

initialCache: When set to false, will skip payload cache for initial fetch (defaults to true).

これだけ見ると単純に毎回キャッシュを無視して取りに行くようにも読めてしまうので、ちょっと不親切かなと思いました。

せっかくこんなに便利な機能なので、今からでも useFetch("/api/user", { swr: true }) とかにリネームした方がわかりやすいんじゃないか……と思っています。ちょっとだけ。

Discussion

Mikihiro SaitoMikihiro Saito

現在useSWRのような動きはしないようです
https://github.com/nuxt/framework/blob/b859e22be467632b28129a25020ffb85339b1067/packages/nuxt/src/app/composables/asyncData.ts#L132-L134

このinitialCacheというオプションは初回ロード時にSSRやSSG時にリクエストがダブらないようにするための変更かと思われます。ハイドレーションで判断できるようになったので不要になり、消したという流れでしょうか。あくまでも現在での動きで、今後の変更はあるかもしれません

ykoizumi0903ykoizumi0903

コメントありがとうございます!
記事を書いた時点では存在していた initialCacheオプションは確かに消えていますが、冒頭に記載した通り、デフォルトで今回の記事で紹介する挙動になったので内容に変更はないという認識でした。

コメントを受けてこちらの手元で改めて検証してみたのですが、2回目のリクエスト時、APIからのレスポンスが返ってくるまでの間は1回目のリクエストのデータのキャッシュが表示されているように見えます。(Nuxt 3.1.1 で実行)

server/api/current-date.ts
import { setTimeout } from "node:timers/promises";
export default defineEventHandler(async () => {
  return setTimeout(3000).then(() => new Date());
});
pages/current-date.vue
<script lang="ts" setup>
const { data, pending, refresh } = useFetch("/api/current-date");
watchEffect(() => console.log(data.value, pending.value));
</script>

<template>
  <div>Current Date: {{ data }}</div>
  <div>isPending: {{ pending }}</div>
  <button @click="refresh()">リロード</button>
</template>

もしキャッシュが残らないのであれば、console.log のログの2行目、リロードを開始して pending が true になったタイミングで null false がログに表示されるはずなので、これは「結果が返ってくるまでの間だけ古いキャッシュを利用する」 useSWR 相当の動きになっていると解釈しています。
もし再現しない条件があったり、認識の違いがあればご指摘頂けるとありがたいです!

(それとは別に、過去の記載のせいでわかりづらい記事になってしまっているので、後日 initialCache に関する記述を削除して修正しようと思います🙇‍♂️)

Mikihiro SaitoMikihiro Saito

検証までありがとうございます!

なるほどですね。ここでいうキャッシュはリクエストを投げにいかないということではなく、次のデータが表示されるまで前のデータを保持するという意図でしたか

となると私のuseSWRに対する認識自体が誤っていそうです。以下の記事のような動きを想定していました

https://blog.cloud-acct.com/posts/nuxt3-usefetch-cashe/

Mikihiro SaitoMikihiro Saito

追記です。自分が実現したいのはこの自動再検証の無効化のようですね
useAsyncDataだと、この制御ができないというのはありそうです

https://swr.vercel.app/ja/docs/revalidation#自動再検証の無効化

useNuxtDataを使うことも考えましたが、ページ遷移して戻った時点でキャッシュが利用されないのであればドキュメントの最適化は意味があるのだろうかという疑問があります

https://nuxt.com/docs/api/composables/use-nuxt-data#optimistic-updates

ykoizumi0903ykoizumi0903

ライブラリとしての useSWR が複数の機能を持っているので混乱を招きやすそうですが、SWRという言葉の本来の意味としてはキャッシュの一時的な保持(revalidate が完了するまでの間は stale なデータを返す)なので、現在の Nuxt3 のデフォルトの挙動は SWR と言って良いと思います。

元々この記事の意図としては、「Nuxt3のデフォルトのキャッシュ挙動をオフにしても、キャッシュそのものが無効化されるわけではなく、再検証が完了するまでは元々のキャッシュが使われる = useSWR 相当ですよ」という発信がしたかったので、この記事の内容そのものが変わったわけではないと考えています。

また、

ページ遷移して戻った時点でキャッシュが利用されないのであれば

というコメントがありますが、ページ遷移を行って戻ってきた場合でも、再検証中は古いデータが表示されることを確認しています。

useAsyncDataだと、この制御ができない

確かに revalidate の細かい制御のオプションは現状ではサポートされていないので、その点は自分で実装する必要がありそうです。

useSWRImmutable 相当である、ページ遷移時の自動再検証の無効化については、オプションとしてはなくなりましたが、例えば immediate オプションを無効化して、データがない場合のみ明示的に refresh を呼ぶなどをすれば、同等の機能が実現できそうに思いました。

pages/current-date.vue
<script setup lang="ts">
const { data, pending, refresh } = useFetch("/api/current-date", {
  immediate: false,
});
if (!data.value) refresh();
</script>

revalidate 条件をより細かく制御したい場合は、 useFecthuseNuxtData を複数箇所で書いてキャッシュの共有を key に依存するのではなく、上記のようなコードを composables にカスタムフックとして切り出して、useFetch または useAsyncData をそちらに1度だけ書く方が制御しやすいのではないかと思います。

以下は revalidateOnFocus: true 相当の挙動をざっくり実装してみたサンプルコードです。
useWindowFocus は VueUse フックを利用しています)

composables/useCurrentDate.ts
export const useCurrentDate = () => {
  const { data, pending, refresh } = useFetch("/api/current-date");

  const focused = useWindowFocus();
  watch(
    () => focused.value,
    (newVal, oldVal) => {
      if (!oldVal && newVal && !pending.value) refresh();
    }
  );

  return { data, pending, refresh };
};
Mikihiro SaitoMikihiro Saito

なるほどですね。たしかに一工夫する必要がありそうです
サンプルコードや検証と、丁寧にご回答ありがとうございました!