Blitz.jsで実装したページをiframeに読み込むとqueryが無限ループする件を回避する


概要

  • Blitz.jsでWebアプリケーションを作っている
  • あるページを外部サービスでiframeで埋め込んで使いたい要件がある
  • あるページはURLクエリに応じたデータを読み込み表示する機能
  • STUDIOのiframeモジュールで読み込ませて検証した

現象

  • STUDIOのデザインエディタ、プレビューともに該当のデータが表示されない
  • Webインスペクタを見たら毎秒数回API(useQuery()のリクエスト)を叩きまくっている
  • ヤバい

原因

  • Blitz.jsでは react-query を内部でラップして実装している。
  • その useQuery() の内部でBlitz.jsの管理するセッションが新しくなったら自動でキャッシュを破棄してrefetchする処理があるようです。
  • 通常のページへのアクセスと違い、iframeに読み込ませた場合(セッションを保持できず?)無限ループ的にrefetchし続けてしまう。
  • 同じ現象についての公式リポジトリのissueがあった

解決方法

sessionMiddleware() の設定を変える

  • issueで提示されている blitz.config.tssessionMiddleware() のオプションで sameSite: "none" を設定する。
  • ただしこれは https://~ のみで動作するとのこと。(npm run dev などlocalhostだと確認できない)し、 secureCookies: true を併用したい場合、Safariだとうまくいかないという話題もある(が英語が自信ないのと確認していないので要検証)

API Routes経由でデータを取得する

  • 自分が採用したのはAPI Routesでデータを取得する方法。
  • 今回の要件は完全にパブリックな埋め込み専用のページなので限定的なユースケースのAPIなので使い回しやビジネスロジックがほとんど不要だったので割り切れるため。

エントリーポイント

  • Next.jsと違って {src}/page/api/* の代わりに {src}/api/* に置く。(pagesのままでも動く)
/api/some-data/[id].ts

ハンドラの実装

import { BlitzApiHandler } from "blitz"
import db from "db"

// Promise<T> -> T にするUtilityType
import Unpromise from "app/utils/Unpromise"
// string | string[] | undefined -> string | undefined にする関数
import parseQueryValue from "app/helpers/parseQueryValue"

const fetchData = async (id: string) => {
  return await db.someData.findFirst({
    where: { id },
    // このページで使いたいデータだけ取り出す。
    select: {
      id: true,
      name: true,
      description: true,
      image: true,
    },
  })
}

type Response = Unpromise<ReturnType<typeof fetchData>>

const handler: BlitzApiHandler<Response> = async (req, res) => {
  const {
    query: { id },
  } = req

  const idParam = parseQueryValue(id)

  if (!idParam) {
    res.statusCode = 400
    res.setHeader("Content-Type", "application/json")
    res.end(JSON.stringify({ error: "idParam is required" }))

    return
  }

  const data = await fetchData(idParam)

  if (!data) {
    res.statusCode = 404
    res.setHeader("Content-Type", "application/json")
    res.end(JSON.stringify({ error: `${idParam} is not found.` }))

    return
  }

  res.statusCode = 200
  res.setHeader("Content-Type", "application/json")
  res.end(JSON.stringify(data))
}

export default handler

export type GetDataResponse = Response

利用するコード

import { GetDataResponse } from '/api/data/[id]'
const fetchData = (id : string) => {
  const response = await fetch(`/api/some-data/${id}`)
  const data = await response.json()
  // 中略
  return data as GetDataResponse // fetch()したレスポンスへのTypeアサーションはご自由に
}

注意点

  • <Suspense> など向けの実装は自分でやる必要がある。