💭

Next.js FirebaseとuseSWRの組み合わせでISRを実装する

2022/03/15に公開

前提知識

  • Next.jsの基礎
  • SG・CSR・ISRについてちょっとわかる
  • Firebase(v9)についてちょっとわかる

狙い

Firebase(v9)で管理しているデータをSGする。そのデータがクライアントサイドで更新されたときに再度ビルドする(ISR)為にuseSWRを使用する。

SG・ISR・CSRについてざっくりおさらい

Next.jsではSG(Static Genaration)が簡単に実装できます。SGとは言葉の通り静的サイトを生成する事です。Reactでは基本的にクライアントサイド(私たち利用者側)でコンポーネントのレンダリングが行われる(CSR)ことでSPAを実現しています。つまり、そのコンポーネントをレンダリングするアクションを起こす必要があり、もともとは空のページなのです。

それに対してSGはビルドした時点であらかじめレンダリングが行われます。そうすることでそのページに訪れる際はレンダリングをする必要がないため、高速になる。SEOが有利になるなどの利点があります。

しかし、静的に生成されているページなのでクライアントサイドで何か変更を加えても反映することができません。そこでISR(Incremental Static Regeneration)という技術を用いる必要があります。

Next.jsではgetStaticPropsという関数を使用することで簡単にSGを実装することができ、その中のオプションであるrevalidateを設定すればISR自体は簡単に実装可能です。
しかしそれだけでは少し足りないのです。ISRの仕様で一度古いキャッシュを返すという現象が発生する為、更新がちゃんと反映されるためには画面を何度か更新する必要があり、ユーザー体験が良いとは言えないでしょう。

詳しくはこちらの記事がとても参考になるかと。(管理人様、ありがとうございます!)
https://zenn.dev/catnose99/articles/8bed46fb271e44

そこでuseSWRというReact Hooks ライブラリを使用することでキャッシュが古くなったのを自動的に更新してくれるようになります。

早速実装してみたいのだが・・・

私自身useSWRを自力で実装するのは初めてで概念自体も殆ど理解していませんでしたのでとても苦労しました💦その苦労を一からたどっていきたいと思います(笑)

まずは公式ドキュメント!

https://swr.vercel.app/ja

公式のまんまです。

import useSWR from 'swr'

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



これを見て私は
『第一引数はエンドポイントを指すURLが必要なんだな!FireStoreのエンドポイントはあるのか???』
と思ってしまいました。
しかし調べても調べてもfireStoreのエンドポイントに関する有力な情報へはたどり着きませんでした・・・。大失敗です。

fetcherはなんだ??

だったら第二引数のfetcherはなんだ??

また公式をそのまま

import fetch from 'unfetch'

const fetcher = url => fetch(url).then(r => r.json())

function App () {
  const { data, error } = useSWR('/api/data', fetcher)
  // ...
}

はいはい、こんな感じの定義なわけね・・・。
このまま使ってもダメだよな。と思ってる時に気づきました。



このurlはどこから来るんだ???
そう!このurlというのはuseSWRの第一引数が渡ってくるのです。

という事はfireStoreにfetchする為にfireStore関係の関数を見てみましょう!

fireStoreのデータを読み取る方法

公式

import { doc, getDoc } from "firebase/firestore";

const docRef = doc(db, "cities", "SF");
const docSnap = await getDoc(docRef);

if (docSnap.exists()) {
  console.log("Document data:", docSnap.data());
} else {
  // doc.data() will be undefined in this case
  console.log("No such document!");
}

このようにdocRefという参照をurlのような形で作成してgetDoc()で取得している事が分かるかと思います。



つまりuseSWRの第一引数に参照のもととなるurl,fetcherはそのurlを引数として受け取りデータを取得する関数を作成すれば良さそうです!

export const fetcher: (url: string) => Promise<Content[]> = async (url) => {
  const docs = await getAllDocIds(url);

  const contents = docs.map(async (id) => {
    return content = await getFirebaseContent(id);
  });

  const results = await Promise.all(contents);
  return results;
};

こちら私が作成したfetcher関数なのですが一部今回の実装とは関係ない部分を省いているので、エラーが出てしまうかもしれません。

const { data, error } = useSWR(`${uid}/${id}`, fetcher)

このような感じにすることで無事fireStoreからuseSWR経由でデータを取得することができました!



私はこの第一引数のurlを

const id = url.split('/')[1];

のようにすることで中の変数を分離してfetcher関数内で使用しましたが、恐らく

useSWR({uid,id},fetcher)

のように第一引数にオブジェクトを指定することもできるのではないかと思います。
すみません。こちらは未確認です。

まとめ

これで何とかFirebaseとuseSWRの組み合わせでISRを実装することができました!
調べてもあまり出てこなかったので、私自身の理解力がかなり低かったことが主な要因なのですが、もし同じように困っている方がいれば参考にしていただけたら幸いです。

今回の学びとしては公式ドキュメントを読むことはもちろん、
それが何を指しているのか、何をする為の物なのか理解する力が必要だなと感じました。
後は変な思い込みをなくすこと。

Discussion