ClerkのReact Componentが便利すぎるので覗いてみるnon_cache

今回は認証・認可サービスであるClerkのSDKについて解説をします。Clerkといえば基本的な認証・認可機能はもちろんのこと提供されているReact Componentを配置するだけで認証フローが完結する特徴があります。


https://clerk.com/

しかし、React Componentを配置するだけで認証が完結するというのはあまりにも簡単でブラックボックス化しているため、内部の実装を確認してその詳細を見ていきたいと思います。

はじめに

本記事ではClerkの提供するSDKに着目して解説をするため、具体的なClerkの使用方法や個別の認証方法については触れません。

また、Next.jsやRemix、TanStack StartなどさまざまなFrameworkに対応していますが、今回はReact + Viteの構成を前提とします。

SDKのGithub Repositoryは下記のリンクです。バージョンは5.9.1を参照します。

https://github.com/clerk/javascript

Quickstart

まずは公式ドキュメントをもとに最もシンプルにClerkを始める流れを整理します。

https://clerk.com/docs/quickstarts/react

ライブラリのInstallと環境変数の設定

Installするべきパッケージは@clerk/clerk-reactのみです。

pnpm add @clerk/clerk-react

@clerk/clerk-reactは内部でSWRを使用していることや共通のShared Packageによって圧縮後でも32.3kBと少し大きめの印象です。

https://bundlephobia.com/package/@clerk/clerk-react@5.9.1

Packageインストール後は必要な環境変数を設定します。VITE_CLERK_PUBLISHABLE_KEYはClerkのダッシュボードから取得が可能です。

.env.local
VITE_CLERK_PUBLISHABLE_KEY=YOUR_PUBLISHABLE_KEY

ClerkProviderを追加する

これまでで必要な設定は完了しました。次にClerkProviderでアプリケーションをWrapします。その際に先ほど設定したPUBLISHABLE_KEYをProviderに渡します。

main.tsx
import <span class="token maybe-class-name">React</span> from 'react'
import <span class="token maybe-class-name">ReactDOM</span> from 'react-dom/client'
import <span class="token maybe-class-name">App</span> from './App.tsx'
import './index.css'
import <span class="token punctuation">{ ClerkProvider }</span> from '@clerk/clerk-react'

// Import your publishable key
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY

if (!PUBLISHABLE_KEY) {
  throw new Error('Missing Publishable Key')
}

ReactDOM.createRoot(document.getElementById('root')!).render(
  <span class="token tag"><span class="token punctuation"><React.StrictMode</span>></span><span class="token plain-text">
    </span><span class="token tag"><span class="token punctuation"><ClerkProvider</span> publishableKey<span class="token script-punctuation punctuation">={PUBLISHABLE_KEY}</span> afterSignOutUrl<span class="token punctuation attr-equals">="/"</span>></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"><App</span> /></span><span class="token plain-text">
    </span><span class="token tag"><span class="token punctuation"></ClerkProvider</span>></span><span class="token plain-text">
  </span><span class="token tag"><span class="token punctuation"></React.StrictMode</span>></span>,
)

HeaderにClerkのComponentを配置する

*公式ドキュメントとコード例を変更しています。

最後にアプリのHeaderにClerkのComponentを配置します。<SignedIn>ユーザがアプリケーションにログインしている場合のみChildrenをレンダリングします。

逆に<SignedOut>ログインしているユーザがいない場合にのみChildrenをレンダリングします。そのため、未ログイン時は<RedirectToSignIn>がレンダリングされ、自動的にSignInページへリダイレクトされます。

header.tsx
import <span class="token punctuation">{ SignedIn, SignedOut, RedirectToSignIn, UserButton }</span> from '@clerk/clerk-react'

export default function App() {
  return (
    <span class="token tag"><span class="token punctuation"><header</span>></span><span class="token plain-text">
      </span>{/* ⭐️ 未ログイン時は↓がレンダリングされ、SignInページへリダイレクトされる */}<span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"><SignedOut</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><RedirectToSignIn</span> /></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"></SignedOut</span>></span><span class="token plain-text">
      </span>{/* ⭐️ ログイン時は↓がレンダリングされる */}<span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"><SignedIn</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><UserButton</span> /></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"></SignedIn</span>></span><span class="token plain-text">
    </span><span class="token tag"><span class="token punctuation"></header</span>></span>
  )
}

その後SignInページへ遷移しますが、SignInのPortalなどはあらかじめClerkが用意してくれているため、このような認証画面などの実装も一切不要です。


https://clerk.com/docs/customization/account-portal/overview#sign-in

また、追加の要件が発生してもClerkのDashboardを変更するか、必要なComponentを追加するだけで認証基盤を拡張させることができます。

ClerkProviderとHooks

これだけ便利なClerkですが内部でどのような実装がされているのかが気になるところです。そこで、上記で解説をしたQuickStarkの実装をソースコードを見ながら確認していきます。

冒頭でも紹介した通りClerkのSDKを提供しているRepositoryは下記のものです。その中のpackages/react配下に@clerk/clerk-reactに関する実装があります。

https://github.com/clerk/javascript/tree/main/packages/react

ClerkProviderが管理するもの

ClerkProviderは以下のContextを管理するProviderです。基本的にはReactのcreateContextを使用してGlobalなStateを管理しています。管理されているContextは以下の6つです。

ClerkContextProvider.tsx
{/* packages/shared/src/react/contexts.tsxにて管理 */}
const [UserContext, useUserContext] = <span class="token function">createContextAndHook<span class="token operator"><UserResource | null | undefined></span></span>('UserContext');

<span class="token tag"><span class="token punctuation"><IsomorphicClerkContext.Provider</span> value<span class="token script-punctuation punctuation">={clerkCtx}</span>></span><span class="token plain-text">
  </span><span class="token tag"><span class="token punctuation"><ClientContext.Provider</span> value<span class="token script-punctuation punctuation">={clientCtx}</span>></span><span class="token plain-text">
    </span><span class="token tag"><span class="token punctuation"><SessionContext.Provider</span> value<span class="token script-punctuation punctuation">={sessionCtx}</span>></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"><OrganizationProvider</span> <span class="token punctuation">{...organizationCtx.value}</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><AuthContext.Provider</span> value<span class="token script-punctuation punctuation">={authCtx}</span>></span><span class="token plain-text">
          </span><span class="token tag"><span class="token punctuation"><UserContext.Provider</span> value<span class="token script-punctuation punctuation">={userCtx}</span>></span>{children}<span class="token tag"><span class="token punctuation"></UserContext.Provider</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"></AuthContext.Provider</span>></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"></OrganizationProvider</span>></span><span class="token plain-text">
    </span><span class="token tag"><span class="token punctuation"></SessionContext.Provider</span>></span><span class="token plain-text">
  </span><span class="token tag"><span class="token punctuation"></ClientContext.Provider</span>></span><span class="token plain-text">
</span><span class="token tag"><span class="token punctuation"></IsomorphicClerkContext.Provider</span>></span>

https://github.com/clerk/javascript/blob/5e0da19123b585d0cbf502f3138076be6c4c126f/packages/react/src/contexts/ClerkContextProvider.tsx

IsomorphicClerkContext

ClientContext

SessionContext

  • これはHTTPセッションを抽象化したObjectです
  • セッションのアクティビティを記録し、クライアントサイドでセッションを終了するためのメソッドなどが含まれています
  • Docs: https://clerk.com/docs/references/javascript/session

OrganizationProvider

AuthContext

UserContext

Contextを扱うためのHooks

先ほどClerkProviderによって管理される状態を整理しました。次にそれらを扱うHooksを見ていきます。基本的には単純にContextを取得して返すだけですが、useOrganizationは内部でロジックを持っています。

useOrganization

OrganizationのProviderを見るとわかるとおりSWRのConfigが設定されています。Organizationの管理においてはデータ取得にSWRが使用されているようです。

例えばuseOrganizationを使用して、Organizationに紐づくmembershipを取得する際などは内部でSWRを使ってデータを取得しています。

https://github.com/clerk/javascript/blob/5e0da19123b585d0cbf502f3138076be6c4c126f/packages/shared/src/react/hooks/useOrganization.tsx#L212-L224

*usePagesOrInfiniteは内部でSWRをWrapした関数です。

https://github.com/clerk/javascript/blob/5e0da19123b585d0cbf502f3138076be6c4c126f/packages/shared/src/react/hooks/usePagesOrInfinite.ts#L96-L111

開発者がClerkを使用する際に直接SWRを意識することはないようにWrapされていますが、SWRのキャッシュなどで何か特殊な挙動に疑問を持った際には参考になるかもしれません。

また、Contextからデータを取得するだけだと思いながらも、意図せずHTTPリクエストが走っている可能性があるので注意が必要です。

各種ComponentとRouter

先ほどClerkが提供するProviderからどのような状態が管理されているかを解説しました。次にQuickStartで紹介したUI Componentがどのように実装され、認証が行われるかを見ていきます。

再掲 header.tsx
import <span class="token punctuation">{ SignedIn, SignedOut, RedirectToSignIn, UserButton }</span> from '@clerk/clerk-react'

export default function App() {
  return (
    <span class="token tag"><span class="token punctuation"><header</span>></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"><SignedOut</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><RedirectToSignIn</span> /></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"></SignedOut</span>></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"><SignedIn</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><UserButton</span> /></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"></SignedIn</span>></span><span class="token plain-text">
    </span><span class="token tag"><span class="token punctuation"></header</span>></span>
  )
}

SignedInとSignedOut

先ほどの例で<SignedIn><SignedOut>はユーザのログイン状態に応じてChildrenを制御するとありましたが、Contextからユーザ情報を取得してレンダリングを制御しているだけです。

https://github.com/clerk/javascript/blob/main/packages/astro/src/react/controlComponents.tsx#L12-L27

そしてユーザが未ログイン状態の場合に<RedirectToSignIn />がレンダリングされてSignInページへリダイレクトされます。

SignInコンポーネント


https://clerk.com/docs/customization/account-portal/overview#sign-in

冒頭でも掲載しましたが、ClerkではこのようなSignIn・SiginUp画面がデフォルトで用意されており開発者が実装を意識することなく配置・カスタマイズができます。

先ほどの例に続き、<RedirectToSignIn />がレンダリングされて/sign-inへリダイレクトされたことを想定します。

SignInに関するComponentはpackages/clerk-js配下に実装されており、独自のRouterによってnavigationが制御されています。Clerkの独自RouterはInterfaceがReact Routerに似ており、シンプルで薄く実装がされています。

Routerに関する実装:https://github.com/clerk/javascript/blob/5e0da19123b585d0cbf502f3138076be6c4c126f/packages/clerk-js/src/ui/router/index.tsx

SignIn.tsxではログインで使用されうる認証画面のルーティングを定義しています。例えば他要素認証の場合<SignInStart />でパスワード認証をしたのちに、/sign-in/factor-twoへ遷移して<SignInFactorTwo />をレンダリングするといった具合です。

SignIn.tsx
function SignInRoutes(): JSX.<span class="token maybe-class-name">Element</span> {
  const signInContext = useSignInContext();

  return (
    <span class="token tag"><span class="token punctuation"><Flow.Root</span> flow<span class="token punctuation attr-equals">='signIn'</span>></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"><Switch</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><Route</span> path<span class="token punctuation attr-equals">='factor-one'</span>></span><span class="token plain-text">
          </span><span class="token tag"><span class="token punctuation"><SignInFactorOne</span> /></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"></Route</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><Route</span> path<span class="token punctuation attr-equals">='factor-two'</span>></span><span class="token plain-text">
          </span><span class="token tag"><span class="token punctuation"><SignInFactorTwo</span> /></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"></Route</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><Route</span> path<span class="token punctuation attr-equals">='reset-password'</span>></span><span class="token plain-text">
          </span><span class="token tag"><span class="token punctuation"><ResetPassword</span> /></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"></Route</span>></span><span class="token plain-text">
        </span>{/* ...省略 */ }<span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><Route</span> index></span><span class="token plain-text">
          </span><span class="token tag"><span class="token punctuation"><SignInStart</span> /></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"></Route</span>></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"><Route</span>></span><span class="token plain-text">
          </span><span class="token tag"><span class="token punctuation"><RedirectToSignIn</span> /></span><span class="token plain-text">
        </span><span class="token tag"><span class="token punctuation"></Route</span>></span><span class="token plain-text">
      </span><span class="token tag"><span class="token punctuation"></Switch</span>></span><span class="token plain-text">
    </span><span class="token tag"><span class="token punctuation"></Flow.Root</span>></span>
  );
}

https://github.com/clerk/javascript/blob/main/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx#L25-L74

SignInStart

/sign-inへ遷移してindexルートの場合レンダリングされるのは<SignInStart>です。

https://github.com/clerk/javascript/blob/5e0da19123b585d0cbf502f3138076be6c4c126f/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx#L60

このコンポーネントが実際のSignIn画面に該当する実装をしています。

下のコードは認証情報が入力されてFormがSubmitされた際に発火する関数です。

https://github.com/clerk/javascript/blob/5e0da19123b585d0cbf502f3138076be6c4c126f/packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx#L273-L301

signIn.create()によってサインイン処理が実行され、statusが"complete"の場合はユーザをActiveにし、追加で認証が必要な場合は先ほど設定したルーティングに従って遷移させています。

そして認証に成功すると<SignedIn>内のchildrenがレンダリングされて認証後の画面が表示されます。これでQueickStartの一連の流れが完了しました。

まとめ

今回は簡単ではありますがQuickStartの挙動が内部でどのように実装されているのか、Clerkが提供するSDKの内部実装を見ていきました。冒頭で紹介したようにClerkはReactのComponentを配置するだけで認証が実装できてしまいます。

その便利さゆえに内部がどのように管理されているか気になったため覗いてみましたが、SDKのpackage全体を通して非常に見通しがよくわかりやすいコードだったと感じました。