【ExpoとSupabaseで作る認証フロー (3)】認証ガードで画面を保護する最終実装
はじめに
前回の記事で、invalidateQueries
を使い、ログインやログアウトといったデータ変更の結果をアプリ全体のUIに自動で反映させる仕組みを構築しました。
シリーズ最終回の今回は、これまでに構築した認証フックを使い、Expo Routerのルートレイアウトファイル (app/_layout.tsx
) で認証ガード(Authentication Guard)を実装します。これにより、ユーザーのログイン状態に応じてアクセスできる画面を自動で制御します。
Step 1: 実装のゴールを定義する
私たちが目指すユーザー体験は以下の通りです。
- ✅ 未ログインのユーザーがアプリ画面(例:
/home
)にアクセスしようとしたら、/signIn
に自動でリダイレクトする。 - ✅ ログイン済みのユーザーがオンボーディング画面(例:
/signIn
)にアクセスしようとしたら、アプリのメイン画面に自動でリダイレクトする。 - ✅ アプリ起動時、ログイン状態の確認が終わるまではスプラッシュスクリーンを表示し続け、画面のちらつきを防ぐ。
Step 2: ロジックをカスタムフックに分離する
これらのリダイレクトロジックを、UIの骨格であるapp/_layout.tsx
にすべて直接書き込むと、コンポーネントが肥大化し見通しが悪くなります。
そこで、「関心の分離」という設計原則に従い、認証状態の監視とリダイレクト実行というロジックを、useProtectedRoute
という新しいカスタムフックにカプセル化(閉じ込める)します。
それでは、このフックをapi/auth/index.ts
に実装しましょう。
// ... 他のフック
import { useRouter, useSegments } from "expo-router";
import { useEffect } from "react";
// ★ 認証ガードのロジックをすべてこのフックにカプセル化
export function useProtectedRoute() {
// 1. 「唯一の情報源」からセッション状態と読み込み状態を取得
const { session, isPending } = useGetSession();
// 2. Expo Routerのフックで、ナビゲーションと現在のURLセグメントを取得
const segments = useSegments();
const router = useRouter();
// 3. セッション状態やパスが変わるたびにリダイレクトロジックを実行
useEffect(() => {
// セッション情報の確認中は、リダイレクト処理を待機
if (isPending) return;
// 現在いるのが認証不要な(onboarding)グループの画面か判定
const inOnboardingGroup = segments[0] === "(onboarding)";
// ケース1: ログインしておらず、保護された画面にいる場合
if (!session && !inOnboardingGroup) {
router.replace("/(onboarding)"); // オンボーディング画面へ
}
// ケース2: ログイン済みで、オンボーディング画面にいる場合
else if (session && inOnboardingGroup) {
router.replace("/(app)/(tabs)"); // アプリのメイン画面へ
}
}, [session, isPending, segments, router]);
// 4. ローディング状態をUI側に渡して、スプラッシュスクリーンの制御に使う
return { isPending };
}
Step 3: ルートレイアウトをシンプルに保つ
認証ロジックをuseProtectedRoute
フックに分離したおかげで、アプリ全体のエントリーポイントであるapp/_layout.tsx
は、驚くほどシンプルで宣言的になります。
このファイルは、プロバイダーの設置やスプラッシュスクリーンの制御といった、レイアウトの関心事にのみ集中できます。
import { useProtectedRoute } from "@/api/auth";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Slot, SplashScreen } from "expo-router";
import { useEffect } from "react";
const queryClient = new QueryClient();
// スプラッシュスクリーンが自動で消えないようにする
SplashScreen.preventAutoHideAsync();
function RootLayoutNav() {
// ★ 認証ロジックを呼び出すが、関心があるのはローディング状態のみ
const { isPending } = useProtectedRoute();
useEffect(() => {
// ローディングが完了したらスプラッシュスクリーンを非表示
if (!isPending) {
SplashScreen.hideAsync();
}
}, [isPending]);
// ローディング中は何も描画しない(結果、スプラッシュスクリーンが表示され続ける)
if (isPending) {
return null;
}
// ローディング完了後、現在のルートに応じた画面を描画
return <Slot />;
}
// アプリケーションの最上位。Providerの設置に専念する
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<RootLayoutNav />
</QueryClientProvider>
);
}
シリーズのまとめ
3回にわたるシリーズを通して、私たちは以下のステップで堅牢な認証フローを構築しました。
-
第一歩:
useMutation
を使い、ユーザーのアクションをトリガーとするデータ変更処理を実装しました。 -
状態の同期:
invalidateQueries
を使い、Mutationの成功をuseQuery
で定義した「唯一の情報源」に自動で反映させる仕組みを構築しました。 -
最終実装: 認証ロジックを
useProtectedRoute
にカプセル化し、UIから分離することで、宣言的でメンテナンス性の高い認証ガードを完成させました。
このアーキテクチャは、関心の分離と信頼できる唯一の情報源という原則に基づいています。この考え方を応用することで、認証以外の様々な機能も、きっとクリーンに実装できるはずです。
このシリーズが、あなたのアプリ開発の助けになれば幸いです。
謝辞
このシリーズの執筆にあたり、以下のZenn Bookを参考にさせていただきました。TanStack Queryの学習に非常に役立つ素晴らしい資料です。執筆者のTaiseiさんに心から感謝申し上げます。
Discussion