はじめに

2か月前に「SWRを使おうぜという話」という記事を書きました。
https://zenn.dev/mast1ff/articles/40b3ea4e221c36

Vercel謹製のSWRの便利さや導入の簡単さについて語りました。
そしてそれに感動を覚えたらとにかく使いたくなります。
だって既存のプロジェクトに導入するのも簡単だから(!)

Firestore

サーバーレスでのアプリケーションやデータをほとんど持たないwebサイトを作成するときは、私はfirebaseのFirestore(GCP)をよく利用します。
これもまたSWRなどと同じくとても簡単に導入できるので、各方面に推奨しております。

しかしFirestoreでの辛い点は、データの取得や更新に必要な手数の多さです。
通常の記載は下記。(※Typescriptを使用しております。)
投稿一つ型チェックするのにこのコーディング量!

type Categories = "ブログ" | "ニュース";
type Post = {
  slug: string;
  title: string;
  category: Categories;
  body: string;
  createdAt: string;
  updatedAt: string;
}

const validate = (data: any): data is Post => {
  if (!(data.slug typeof data.slug === "string")) {
    return false;
  }
  if (!(data.title typeof data.title === "string")) {
    return false;
  }
  const cat: Categories[] = ["ブログ", "ニュース"] as const;
  if (!(data.category cat.some((c) => data.category === c))) {
    return false;
  }
  if (!(data.body typeof data.body === "string")) {
    return false;
  }
  /** ...などのバリデーションを記載 */
  return true;
};

const toFirestore = (post: Post): DocumentData => {
  return {
    slug: post.slug,
    title: post.title,
    category: post.category,
    body: post.body,
    createdAt: post.createdAt,
    updatedAt: post.updatedAt
  };
};

const fromFirestore = (
  snampshot: QueryDocumentSnapshot,
  options: SnapshotOptions
): Post => {
  const data = snapshot.data(options)!;
  if (!validate(data)) {
    throw new Error('invalid!');
  }
  return {
    slug: data.slug,
    title: data.title,
    category: data.category,
    body: data.body,
    createdAt: data.createdAt,
    updatedAt: data.updatedAt
  };
};

const converter = {
  toFirestore,
  fromFirestore
};

/** 取得側 */
const fetchPosts = async () => {
  const _: Post[] = [];
  const doc = await firebase
    .firestore()
    .collection('/posts')
    .withConverter(converter)
    .get();
  if (doc.exists) {
    doc.forEach((d) => {
    _.push(d.data());
    });
  }
  return _;
};

ここで私は、SWRをFirestoreを一緒に使ったらもっと楽なのに と思ってしまいました。
ですので、さっきのfetchPostsをuseSWRに渡してみます。
※SWRの使い方は公式ドキュメント及び前回の記事を参照ください。
https://swr.vercel.app/

/** 省略 */
const { data: posts } = useSWR('firestore/posts', fetchPosts);

SWRはポーリングフェッチを行う

しかしながらこれには大きな罠がありました。
SWRは数秒に一度データの検証を行い、差分を検知することで変更をフロントエンドに反映させることが出来る機能を持っています。
そしてこの機能はデフォルトでONとなっています。
※この機能はデフォルトではOFFとなっております。(コメント内でご指摘いただきました。ありがとうございます)

この時点で私は脳死状態で実装したため、firestoreの使用量を確認すると10件程度のドキュメントしかないにも関わらずわずか1時間たらずで800件近くのGET!
気づかずに本番環境で実行して、万が一にもサイトが跳ねた場合とんでもない取得量・使用量になり課金まっしぐらです。

今回の教訓は、**SWRとfirestoreの相性はあまりよくない しっかりとドキュメントを読み込んでから実装する**。

どうしても使用する場合は今回の場合で言えば

/**
 * コメント内でご指摘いただきました。ありがとうございます
 */
const { data: posts, revalidate } = useSWR("firestore/posts", fetchPosts, {
  refreshInterval: <number>
});

で再検証時間を適切に取る。

また、フォーカス時の再検証を行う場合は、

const { data: posts, revalidate } = useSWR("firestore/posts", fetchPosts, {
  revalidateOnFocus: false,
  revalidateOnReconnect: false
});

再検証を止めて、再フェッチしたいタイミングでrevalidateイベントを発火させる。

または、

const { data: posts } = useSWR("firestore/posts", fetchPosts, {
    focusThrottleInterval: <number>
});

などでフォーカス時の再検証を許容する間隔に幅を取る。

などの方法を取ることができます。


今回は失敗の備忘録も兼ねて短編でした。

※コードの記載は記事用に即席のため、ミスがあったらごめんなさい。


2021/08/05 記事内のご指摘いただいたミスを修正致しました。