メインコンテンツまでスキップ

状態に応じたナビゲーション

ToDo一覧画面へ遷移できるようになりましたが、ただ画面を移動しているだけで認証制御はできていません。 ユーザの認証状態に応じて画面を切り替えるように修正しましょう。

React Navigationでは、状態に応じてスタック定義を切り替えることができます。 そのコード例を次に示します。

isSignedIn ? (
<>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="TodoBoard" component={TodoBoardScreen} />
</>
) : (
<>
<Stack.Screen name="Welcome" component={SignInScreen} />
<Stack.Screen name="Login" component={SignUpScreen} />
</>
)

この方法は、React Navigationの公式ドキュメントで紹介されているものです。 ToDoアプリでもこれに従い、認証制御を組み込んでいきます。

備考

認証フローの詳細な説明は、React Navigationの公式ドキュメントにあるAuthentication flowsを参照してください。

認証関連の部品

まずは認証制御に必要な部品を用意します。

注記

用意する部品はWebベースのハンズオンを参考にしています。 そこで用意されているBackendService.tsを次のとおり機能分割している点を除き、ほぼ同等機能です。

  • AuthService: 認証機能
  • TodoService: ToDo機能

上記理由から、本ハンズオンでの部品説明は省略します。

次のファイルを追加・修正してください。

  • src/services/AuthService.ts
  • src/services/index.ts
  • src/contexts/UserContext.tsx
  • src/App.tsx
src/services/AuthService.ts
const refreshCsrfToken = async () => {};

const signup = async (userName: string, password: string) => {
return Promise.resolve();
};

const login = async (userName: string, password: string) => {
return Promise.resolve();
};

const logout = async () => {
return Promise.resolve();
};

export const AuthService = {
signup,
login,
logout,
refreshCsrfToken,
};
src/services/index.ts
export * from './AuthService';
src/contexts/UserContext.tsx
import React, {useContext, useState} from 'react';
import {AuthService} from 'services/AuthService';

export class AccountConflictError {}

export class AuthenticationFailedError {}

interface ContextValueType {
signup: (userName: string, password: string) => Promise<void | AccountConflictError>;
login: (userName: string, password: string) => Promise<void | AuthenticationFailedError>;
logout: () => Promise<void>;
userName: string;
isLoggedIn: boolean;
}

export const UserContext = React.createContext<ContextValueType>({} as ContextValueType);

export const useUserContext = () => useContext(UserContext);

export const UserContextProvider: React.FC<React.PropsWithChildren> = ({children}) => {
const [userName, setUserName] = useState<string>('');

const contextValue: ContextValueType = {
signup: async (userName, password) => {
await AuthService.signup(userName, password);
},
login: async (userName, password) => {
await AuthService.login(userName, password);
await AuthService.refreshCsrfToken();
setUserName(userName);
},
logout: async () => {
await AuthService.logout();
await AuthService.refreshCsrfToken();
setUserName('');
},
userName,
isLoggedIn: userName !== '',
};

return <UserContext.Provider value={contextValue}>{children}</UserContext.Provider>;
};
src/App.tsx
  import {NavigationContainer} from '@react-navigation/native';
+ import {UserContextProvider} from 'contexts/UserContext';
import {RootStackNav} from 'navigation';
import React from 'react';
import {StyleSheet} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';

export const App = () => {
return (
<GestureHandlerRootView style={StyleSheet.absoluteFill}>
+ <UserContextProvider>
<NavigationContainer>
<RootStackNav />
</NavigationContainer>
+ </UserContextProvider>
</GestureHandlerRootView>
);
};

状態に応じたナビゲーション

次に、ナビゲーション定義を修正します。修正イメージは以下のとおりです。

  • (今の)RootStackを未認証時の画面を定義するUnauthedStackに変更
  • 認証時の画面を定義するAuthedStackを追加
  • 認証状態によりスタック定義を切り替えるRootStackをルートに追加

AuthStack

src/navigation/RootStackNav.tsxのファイル名をUnauthedStackNav.tsxに変更し、次のように修正してください。

src/navigation/UnauthedStackNav.tsx
  import {useNavigation} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import React from 'react';
import {Button} from 'react-native-elements';
- import {Welcome, Instructions, Login, TodoBoard} from 'screens';
+ import {Welcome, Instructions, Login} from 'screens';

const HeaderRight: React.FC = () => {
const navigation = useNavigation();
return <Button type="clear" onPress={() => navigation.navigate('Login')} title="ログイン" />;
};

const nav = createStackNavigator();
- export const RootStackNav: React.FC = () => {
+ export const UnauthedStackNav: React.FC = () => {
return (
<nav.Navigator
initialRouteName={Welcome.name}
screenOptions={{
headerRight: () => <HeaderRight />,
}}>
<nav.Screen
name="Welcome"
component={Welcome}
options={{
headerTitle: 'Welcome',
}}
/>
<nav.Screen
name="Login"
component={Login}
options={{
headerTitle: 'ログイン',
headerRight: undefined,
}}
/>
- <nav.Screen
- name="TodoBoard"
- component={TodoBoard}
- options={{
- headerTitle: 'Todo',
- headerRight: undefined,
- }}
- />
<nav.Screen name="Instructions" component={Instructions} />
</nav.Navigator>
);
};

次のファイルを追加・修正してください。

  • src/navigation/AuthedStackNav.tsx
  • src/navigation/RootStackNav.tsx
  • src/navigation/index.ts
src/navigation/AuthedStackNav.tsx
import {createStackNavigator} from '@react-navigation/stack';
import React from 'react';
import {TodoBoard} from 'screens';

const nav = createStackNavigator();
export const AuthedStackNav: React.FC = () => {
return (
<nav.Navigator screenOptions={{headerShown: false}}>
<nav.Screen name="Todo" component={TodoBoard} />
</nav.Navigator>
);
};
src/navigation/RootStackNav.tsx
import {createStackNavigator} from '@react-navigation/stack';
import {useUserContext} from 'contexts/UserContext';
import React from 'react';

import {AuthedStackNav} from './AuthedStackNav';
import {UnauthedStackNav} from './UnauthedStackNav';

const rootNav = createStackNavigator();
export const RootStackNav: React.FC = () => {
const userContext = useUserContext();
return (
<rootNav.Navigator screenOptions={{headerShown: false}}>
{userContext.isLoggedIn ? (
<rootNav.Screen name="Authed" component={AuthedStackNav} />
) : (
<rootNav.Screen name="Unauthed" component={UnauthedStackNav} />
)}
</rootNav.Navigator>
);
};
src/navigation/index.ts
  export * from './RootStackNav';
+ export * from './UnauthedStackNav';
+ export * from './AuthedStackNav';

以上で、認証状態に応じたナビゲーション定義への修正が完了しました。

認証処理

次は、ログインボタンを修正します。

ボタンを押した際にuserContext.login(userName, password)を呼び出して認証状態にします。 userNamepasswordを入力するフォームはログイン画面の実装で作成するため、今は固定値を渡します。

src/screens/auth/Login.tsxを修正してください。

src/screens/auth/Login.tsx
- import {useNavigation} from '@react-navigation/native';
+ import {useUserContext} from 'contexts/UserContext';
- import React from 'react';
+ import React, {useCallback} from 'react';
import {StyleSheet, View} from 'react-native';
import {Text, Button} from 'react-native-elements';

export const Login: React.FC = () => {
- const navigation = useNavigation();
+ const userContext = useUserContext();
+
+ const login = useCallback(() => {
+ // 後で例外処理を追加するので、今はlintのエラーは無視してください。
+ userContext.login('user', 'password');
+ }, [userContext]);

return (
<View style={styles.container}>
<Text h1>ログイン</Text>
- <Button onPress={() => navigation.navigate('TodoBoard')} title="ログイン" />
+ <Button onPress={login} title="ログイン" />
</View>
);
/* ~省略~ */

最後に、ToDo一覧画面にuserContext.logout()を呼びだすログアウトボタンを追加します。

src/screens/todo/TodoBoard.tsxを修正してください。

src/screens/todo/TodoBoard.tsx
+ import {useUserContext} from 'contexts/UserContext';
- import React from 'react';
+ import React, {useCallback} from 'react';
import {StyleSheet, View} from 'react-native';
- import {Text} from 'react-native-elements';
+ import {Text, Button} from 'react-native-elements';

export const TodoBoard: React.FC = () => {
+ const userContext = useUserContext();
+
+ const onLogout = useCallback(() => userContext.logout(), [userContext]);
+
return (
<View style={styles.container}>
<Text h1>Todo</Text>
+ <Button type="clear" onPress={onLogout} title="ログアウト" />
</View>
);
};
/* ~省略~ */

修正できたら実行してください。 次の操作ができたら成功です。

  • ログイン画面からログインしてToDo一覧画面が表示
  • ToDo一覧画面でログアウトするとWelcome画面が表示

Logout