認証フローの実装例
多くのアプリケーションでは、ユーザーのログイン状態に応じて表示する画面を切り替える必要があります。Expo Routerでは、こういった認証フローを効率的に実装するための仕組みが提供されています。
認証フローの基本的なアプローチは以下の通りです。
- ReactのコンテキストAPIでアプリ全体の認証状態を管理
- ルートレイアウトで認証状態に応じた画面制御
- キュアストレージによる認証情報の永続化
- カスタムフックを通じたコンポーネント間での状態共有
詳細なベストプラクティスについては、公式ドキュメントでも解説されています。
今回は雰囲気をつかんでもらえるよう、簡易版の実装を解説します。まずは、認証フローに関わる主要なファイルとその関係を整理しておきましょう(図2)。
中心となるのはcontexts/AuthContext.tsxで、アプリ全体の認証状態を管理します。ここには SessionProvider(コンテキストプロバイダー)、useSession(カスタムフック)、signIn/signOut(認証処理メソッド)が含まれます。
app/_layout.tsx ではuseSessionを使って認証状態を取得し、ログイン済み・未ログインに応じた画面を条件分岐で表示します。app/signin.tsx はログイン画面で、signInメソッドを呼び出して認証処理を実行します。また、expo-secure-storeが認証トークンの永続化を担当し、アプリ起動時の状態復元を可能にします。
認証コンテキストの設定
まずは、ReactのコンテキストAPIを使用してアプリ全体で認証状態を共有できるようにします(リスト3)。
import React, { createContext, useContext, useEffect, useState } from 'react';
import * as SecureStore from 'expo-secure-store';
// 認証状態を管理するコンテキストを作成
const AuthContext = createContext(null);
// 認証状態にアクセスするためのカスタムフック
export function useSession() { // (3)
const value = useContext(AuthContext);
if (!value) {
throw new Error('useSession must be wrapped in a <SessionProvider />');
}
return value;
}
// 認証コンテキストを提供するプロバイダーコンポーネント
export function SessionProvider({ children }) {
// 認証セッション(トークン)を保持するstate
const [session, setSession] = useState(null);
// 初期化中かどうかを示すローディング状態
const [isLoading, setIsLoading] = useState(true);
// アプリ起動時に保存済みのセッション情報を復元
useEffect(() => {
async function loadSession() {
try {
// (2) セキュアストレージから認証トークンを取得
const token = await SecureStore.getItemAsync('authToken');
setSession(token);
} finally {
// 成功・失敗に関わらずローディング状態を終了
setIsLoading(false);
}
}
loadSession();
}, []);
// コンテキストに渡すメソッドと状態をまとめたオブジェクト
const value = {
// ログイン処理:トークンをセキュアストレージに保存し、セッション状態を更新
signIn: async (token) => {
await SecureStore.setItemAsync('authToken', token); // (1)
setSession(token);
},
// ログアウト処理:トークンを削除し、セッション状態をクリア
signOut: async () => {
// 本来はここでログアウトのための通信処理を行う
await SecureStore.deleteItemAsync('authToken');
setSession(null);
},
session, // 現在のセッション状態
isLoading, // ローディング状態
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
(1)のsignInメソッドはログイン処理が成功したときに呼び出され、認証トークンの永続化を行います。今回はセキュアなデータ保存に優れたexpo-secure-storeを使用しました。ここで保存したデータは、(2)の処理を通してメモリに読み込まれ、(3)のカスタムフックを通じてコンポーネントから読み出されます。
ルートレイアウトでの認証制御
リスト3で設定した認証状態に応じた画面制御は、ルートレイアウト(app/_layout.tsx)で行います(リスト4)。
import { Stack } from 'expo-router';
import { SessionProvider, useSession } from '../contexts/AuthContext';
import { Text, View } from 'react-native';
function RootLayoutNav() {
const { session, isLoading } = useSession(); // (1)
if (isLoading) {
return <Text>読み込み中...</Text>;
}
return (
<Stack>
{session ? ( // (2)
// ログイン済み
<>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ title: '設定' }} />
</>
) : (
// 未ログイン
<Stack.Screen name="signin" options={{ title: 'サインイン' }} />
)}
</Stack>
);
}
export default function RootLayout() {
return (
<SessionProvider>
<RootLayoutNav />
</SessionProvider>
);
}
最上位のコンポーネントであるapp/_layout.tsxでは、(1)でコンテキストから認証状態を取得し、(2)の条件分岐でログイン済みかどうかに応じて表示する画面を切り替えています。認証状態の明示的な更新を行いたい場合は、useSessionやAuthContextの実装を拡張して、任意のタイミングでチェックすることで実現できます。
サインイン画面の実装
次に、サインイン画面では、認証コンテキストのsignIn関数を使用してログイン処理を行います(リスト5)。
import React from 'react';
import { View, TextInput, Button } from 'react-native';
import { useSession } from '../contexts/AuthContext';
import { useRouter } from 'expo-router';
export default function SignIn() {
const { signIn } = useSession();
const router = useRouter();
const handleSignIn = async () => {
const token = 'dummy-token'; // (1) 実際はAPIで認証
await signIn(token);
router.replace('/'); // メイン画面へ
};
return (
<View style={{ padding: 20, justifyContent: 'center', flex: 1 }}>
<TextInput placeholder="メールアドレス" />
<TextInput placeholder="パスワード" secureTextEntry />
<Button title="ログイン" onPress={handleSignIn} />
</View>
);
}
実際のアプリでは(1)の箇所でログイン処理の通信を行います。前述の通り、 signInの呼び出しが成功すると<SessionProvider>の内部状態が書き換わり、リスト4の条件分岐によってログイン済みの画面が表示されるようになります。
このように、Expo Routerと認証コンテキストを組み合わせることで、ユーザーの認証状態に応じたスムーズな画面遷移を実現できます。
