Remixでroot.tsxにClerkのUI Componentを使いたい

提供されたUI Componentをレイアウト要素で使用する際の覚書。Remixを使用する場合、app/route.tsxにコンポーネントを配置。ヘッダーナビゲーションコンポーネント内にログインやログアウトボタンを配置したいが、エラーが発生。ClerkProviderの外でUI Componentを使うことがエラーの原因。Layout要素をProviderの内側に配置すると問題解消。ただし、キャッシュとSSRに関して懸念があり、Cloudflareのレポートを参考に検証を行う予定。

広告ここから
広告ここまで

目次

    Clerkの提供するUI Componentをレイアウト要素で使用した時の覚書です。

    やりたいこと

    ヘッダーナビゲーションやフッターなどは、Remixだとapp/route.tsxにコンポーネントを配置します。そして今回はヘッダーのナビゲーションコンポーネントの中に、ログインログアウト系の操作を行うボタンを配置しようとしました。

    試したコード

    まずはClerkのクイックスタートを見ながら実装してみます。ただしコンポーネントの実装先はapp/root.tsxに変えてみました。

    
    export function Layout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en" className='h-full bg-gray-100'>
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            </script>
            <Meta />
            <Links />
          </head>
          <body>
            <Header><SignInButton /></Header>
            <div className='py-10'>
              {children}
            </div>
            <Footer />
            <ScrollRestoration />
            <Scripts />
          </body>
        </html>
      );
    }
    
    function App() {
      return <Outlet />;
    }
    
    export default ClerkApp(App);

    このコードを実行すると、つぎのようなエラーが出てきます。

    Uncaught Error: @clerk/clerk-react: SignedIn can only be used within the <ClerkProvider /> component. Learn more: https://clerk.com/docs/components/clerk-provider

    app/root.tsxClerkProviderの外側になるらしい?

    エラーログを深堀したところ、LayoutコンポーネントがClerkProviderの外にあることがわかりました。ClerkAppの実装は次のようなものです。そのため、どうもLayoutがProviderの外に配置されている様子です。

    export function ClerkApp(App: () => JSX.Element, opts: ClerkAppOptions = {}) {
      return () => {
        let clerkState;
        const isSpaMode = inSpaMode();
    
        // Don't use `useLoaderData` to fetch the clerk state if we're in SPA mode
        if (!isSpaMode) {
          const loaderData = useLoaderData<{ clerkState: any }>();
          clerkState = loaderData.clerkState;
        }
    
        if (isSpaMode) {
          assertPublishableKeyInSpaMode(opts.publishableKey);
        }
    
        return (
          <ClerkProvider
            /* @ts-ignore The type of opts cannot be inferred by TS automatically because of the complex
             * discriminated unions required for the router props and multidomain feature   */
            {...(opts as RemixClerkProviderProps)}
            clerkState={clerkState}
          >
            <App />
          </ClerkProvider>
        );
      };
    }

    ClerkAppの代わりにClerkProviderを利用して対応

    ClerkProviderの外側でUI Componentを使うのがエラーの原因ということは、Layout要素がProviderの内側に入れば良いはずです。そこでこのような実装にしました。

    export function Layout({ children }: { children: React.ReactNode }) {
      const loaderData = useLoaderData<{ clerkState: any }>();
      return (
        <html lang="en" className='h-full bg-gray-100'>
          <head>
            <meta charSet="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <script
              async
              src="https://js.stripe.com/v3/pricing-table.js">
            </script>
            <Meta />
            <Links />
          </head>
          <body>
            <ClerkProvider clerkState={loaderData.clerkState}>
              <Header><SignInButton /></Header>
              <div className='py-10'>
                {children}
              </div>
              <Footer />
              <ScrollRestoration />
              <Scripts />
            </ClerkProvider>
          </body>
        </html>
      );
    }

    今の所このコードで動作してくれている様子です。

    キャッシュについては要検証・気がかりな点

    これで解決・・・に見えるのですが、気がかりなのはキャッシュとSSRまわりです。アプリケーション全体をProvider配下に置いているため、アプリケーションのキャッシュがあまりされない可能性があるかな・・・?と思っています。この辺りはCloudflareのレポートを見ながら様子見していこうと思います。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark