Morising Tech Blog
JAEN

Next.js + SupabaseでSSRページとNonSSRページを使い分ける

公開日:

Next.jsでSupabaseを使ってこのブログを構築している時、SSRするページとそうでないページでSupabase Clientを使ってのやりとりがある場合少しはまったので残しておきます。

この記事の要約

  • SSRページとクライアントページでSupabase Clientを使い分ける必要がある
  • サーバーはcreateServerClient、クライアントはcreateBrowserClientを使用
  • サーバー用APIをクライアントで使うとエラーになるので、APIはサーバー用・クライアント用で別ファイルに分割
  • 小規模ならAPIを用途ごとに分けて管理すればOK。大規模ならServer ComponentやRoute Handlerの利用も検討

問題の内容

Supabaseは、Next.jsのサーバーコンポーネントとクライアントコンポーネントで別のSupabase Clientを使わなければならないという前提がある。

まず、ブログ記事の詳細ページについてはほとんどがSSR対象ページになるだろうという予想の元、page.tsxから記事の内容を取得するAPIを作成するときにこちらのQuick Startのドキュメントに従ってサーバー用のSupabase Clientを使用。

import { createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; export async function createClient() { const cookieStore = await cookies(); return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, ...

そのClientを使ってこちらのようにgetPostByIdというAPIを定義。

import { createClient } from "../../supabase/server"; export async function getPostById(id: number): Promise<Post> { const supabase = await createClient(); const { data, error } = await supabase .from("posts") .select() .eq("id", id) .single(); // Return the data... }

それを使ったPostDetailPageを作成。

const PostDetailPage = async ({ params }: PostDetailPageProps) => { const { id, lang } = await params; const post = await getPostById(Number(id)); // Return the component using post data...

ここまでは全く問題なく。

次に投稿したブログ記事を更新するための画面をクライアントサイドのレンダリングページとして作成する。ここで、updatePostはクライアントサイドのSupabase Clientを使用。

import { createBrowserClient } from "@supabase/ssr"; export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, ...

そのClientを使ってAPIを定義。

import { createClient } from "../../supabase/client"; export async function updatePost( id: number, post: PostFormData, ): Promise<void> { const supabase = createClient(); ...

先ほどのgetPostByIdupdatePostの両方を"use client"(クライアントサイド)のレンダリングページで使おうとしたところで問題発生。

"use client"; import { getPostById } from "@/lib/api/server/posts"; import { updatePost } from "@/lib/api/client/posts"; // ... export default function EditPage() { const params = useParams(); const { id } = params; const [post, setPost] = useState<any>(null); useEffect(() => { (async () => { const data = await getPostById(Number(id)); setPost(data); })(); }, [id]); // ... return ( <UpdatePostForm initialValues={post} onSubmit={async (values) => { await updatePost(post.id, values); }} submitLabel="Update Post" /> )

以下がエラー内容

Ecmascript file had an error
  1 | import { createServerClient } from "@supabase/ssr";
> 2 | import { cookies } from "next/headers";
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  3 |
  4 | export async function createClient() {
  5 |   const cookieStore = await cookies();

You're importing a component that needs "next/headers". That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/app/building-your-application/rendering/server-components

理由は単純でクライアントサイドで先ほどサーバーサイドAPIとして定義したgetPostByIdを使おうとしていたから。エラーによると"next/headers"パッケージはサーバーサイドでのみ利用可能とのこと。

解決策

単純にgetPostByIdをクライアントサイド用にも用意し、クライアントサイド用のSupabase Clientを使うようにした。同じ名前のfunctionが2つ別のファイルに存在することになるが、呼び出す側がサーバーサイドかクライアントサイドかでどちらか一つしか使わないはずなので、問題にはならない。

lib └── api    ├── client    │   └── posts.ts # getPostById, updatePost    └── server    └── posts.ts # getPostById

呼び出し側はこちら

"use client"; import { getPostById, updatePost } from "@/lib/api/client/posts"; // ... export default function EditPage() { // Use the APIs... ...

一応こちらで問題が解決。

一応補足で、大規模になってくるとこのやり方ではなく、Server Componentでデータ取得し、Client Componentにpropsとして渡す設計や、Route Handler / Server Actions を介するのが推奨される。ただし今回は非常に小規模のプロジェクトのため、実装の単純さを優先し、Client 用 API を分ける構成を採用した。

ホームに戻る