Remix Clerk で`clerkState`エラーが出た時の解決方法
Remix と Clerk 連携時に発生する「clerkState」エラーの原因と解決方法を解説。Layout コンポーネントから ClerkProvider を削除し、ClerkApp のみで認証状態を管理する正しい実装パターンを紹介します。
目次
Remix と Clerk を組み合わせた Web アプリケーションを開発中に、予期しない認証エラーに遭遇しました。
Unexpected Server Error
Error: 🔒 Clerk: Looks like you didn't pass 'clerkState' to "<ClerkProvider clerkState={...}>".
Use 'rootAuthLoader' as your root loader. Then, simply wrap the App component with ClerkApp and make it the default export.
この記事では、このエラーの原因と解決方法について解説していきます。
環境構成
今回のプロジェクトで使用している環境は以下の通りです。
- Remix: v2.15.3
- Clerk: @clerk/remix v4.5.5
発生した問題について
開発サーバーを起動してアプリにアクセスすると、Error: 🔒 Clerk: Looks like you didn't pass 'clerkState' to "<ClerkProvider clerkState={...}>".
というエラーが表示され、アプリケーションが正常に動作しませんでした。エラーメッセージから、clerkState
の受け渡しに問題があることは分かりましたが、具体的な解決方法がすぐには見つかりませんでした。
原因の調査
初期の実装(問題のあるコード)
最初の実装では、以下のような構造で Clerk を設定していました。
// app/root.tsx(問題のあるバージョン)
import { ClerkApp, ClerkProvider } from "@clerk/remix";
import { rootAuthLoader } from "@clerk/remix/ssr.server";
import { useLoaderData } from "@remix-run/react";
export const loader = (args: LoaderFunctionArgs) => {
return rootAuthLoader(args, {
publishableKey: (args.context.cloudflare.env as any).CLERK_PUBLISHABLE_KEY,
secretKey: (args.context.cloudflare.env as any).CLERK_SECRET_KEY,
});
};
export function Layout({ children }: { children: React.ReactNode }) {
const loaderData = useLoaderData<typeof loader>();
return (
<ClerkProvider clerkState={loaderData as any} publishableKey={(loaderData as any)?.publishableKey || ""}>
<html lang="ja">
{/* ... */}
<body>
{children}
</body>
</html>
</ClerkProvider>
);
}
function App() {
return <Outlet />;
}
export default ClerkApp(App, {
publishableKey: process.env.CLERK_PUBLISHABLE_KEY || ""
});
問題の特定
この実装には複数の問題がありました。
まず、Layout
コンポーネント内で ClerkProvider
を使用していることです。Remix v2 の Layout
コンポーネント内で useLoaderData
を使用して Clerk の状態を取得しようとしていましたが、これは推奨されるパターンではありませんでした。
次に、ClerkApp
で App をラップしているにも関わらず、Layout
内でも ClerkProvider
を使用していたことです。これにより、認証の状態管理が二重になってしまい、エラーの原因となっていました。
さらに、Clerk の公式ドキュメントで推奨されている実装パターンと大きく異なっていたことも問題でした。独自の実装を試みた結果、フレームワークとライブラリの想定する動作から外れてしまっていたのです。
解決方法
正しい実装
Clerk の公式ドキュメントに従って、以下のように修正しました。
// app/root.tsx(修正後)
import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/cloudflare";
import {
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { ClerkApp } from "@clerk/remix";
import { rootAuthLoader } from "@clerk/remix/ssr.server";
import "./tailwind.css";
export const loader = (args: LoaderFunctionArgs) => {
return rootAuthLoader(args, {
publishableKey: (args.context.cloudflare.env as any).CLERK_PUBLISHABLE_KEY,
secretKey: (args.context.cloudflare.env as any).CLERK_SECRET_KEY,
});
};
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body className="min-h-screen bg-gray-50 font-sans text-gray-900 antialiased">
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
function App() {
return <Outlet />;
}
export default ClerkApp(App);
修正のポイント
修正で最も重要だったのは、Layout
コンポーネントから ClerkProvider
を完全に削除したことです。useLoaderData
の使用も停止し、レイアウトは純粋に UI の構造だけを担当するようにしました。
また、ClerkApp(App)
のみで App をラップするようにし、追加の設定パラメータを削除しました。これにより、Clerk の内部実装に任せることができるようになりました。
最終的に、rootAuthLoader
を loader
として使用し、Clerk の認証状態管理を ClerkApp
に完全に委譲する標準的な Remix 構造を採用しました。この方法により、フレームワークとライブラリが意図した通りに動作するようになったのです。
動作確認
修正後、開発サーバーを再起動してアプリにアクセスすると、正常な HTML レスポンスが返されるようになりました。
curl -s http://localhost:5173/ | head -20
Clerk のエラーは完全に解消され、認証機能も期待通りに動作するようになりました。
まとめ
Remix + Cloudflare Pages + Clerk の組み合わせでは、シンプルな実装を心がけることが大切です。ClerkApp
で App コンポーネントをラップするだけで十分であり、Layout
コンポーネント内で ClerkProvider
を手動設定する必要はありません。rootAuthLoader
を適切に設定すれば、Clerk が自動的に認証状態を管理してくれます。
この経験を通じて、フレームワークとライブラリの組み合わせでは、それぞれの責任範囲を理解し、公式ドキュメントに従うことの重要性を再認識しました。特に認証のような重要な機能では、独自の実装よりも実績のあるパターンを採用することで、開発効率と保守性の両方を向上させることができるでしょう。
参考資料
この記事が同様の問題に遭遇した開発者の助けになれば幸いです。