React Navigation v5 が推奨する認証フローの実装

# はじめに
[前回の記事](https://qiita.com/dhythm/items/56f0671d9d4ec3ce2508)に続いて、 React Navigation v5 に関する記事です。

Version 5.x では、公式ドキュメントで[推奨の認証フロー](https://reactnavigation.org/docs/auth-flow/)が紹介されています。
今回は、その認証フローの実装方法に関して記事にまとめました。

# 下準備
認証フローを実装するにあたり、アプリ全体で状態を管理する必要があります。
この記事では、 Context API を利用して実装します。

### 型定義

利用する型を定義します。
必要に応じて、`Error`などの状態・処理も追加してください。

```tsx
type State =
  | { status: 'Unauthenticated'; token: string }
  | { status: 'Loading';}
  | { status: 'Authenticated'; token: string };

type Action =
  | { type: 'START_LOGIN' }
  | { type: 'COMPLETE_LOGIN'; token: string }
  | { type: 'COMPLETE_LOGOUT' };

type Dispatch = (action: Action) => void
```


### Reducer

各アクションにおける取りうる状態を定義します。

```tsx
const authReducer = (prevState: State, action: Action): State => {
  switch (action.type) {
    case 'START_LOGIN':
      return {
        ...prevState,
        status: 'Loading',
      };
    case 'COMPLETE_LOGIN':
      return {
        ...prevState,
        status: 'Authenticated',
        token: action.token,
      };
    case 'COMPLETE_LOGOUT':
      return {
        ...prevState,
        status: 'Unauthenticated',
        token: undefined,
      };
  }
};
```

### コンテクスト

アプリの状態、ディスパッチャーそれぞれのコンテクストを定義します。

```tsx
const AuthStateContext = createContext<State>({
  status: 'Unauthenticated',
  token: undefined,
});

const AuthDispatchContext = createContext<Dispatch | undefined>(undefined);
```

### 自作 Hooks

慣習に従い、`useXXXX`の形で Hooks を定義します。

```tsx
export const useAuthState = () => {
  const context = React.useContext(AuthStateContext);
  return context;
};

export const useAuthDispatch = () => {
  const context = React.useContext(AuthDispatchContext);
  return context;
};
```

### プロバイダー

アプリ全体で状態を扱えるように、プロバイダーを定義し、値を伝播させます。

```tsx
interface Props {
  readonly children: React.ReactNode;
}

const AuthProvider: React.FunctionComponent<Props> = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, {
    status: 'Unauthenticated',
    token: undefined,
  });

  return (
    <AuthStateContext.Provider value={state}>
      <AuthDispatchContext.Provider value={dispatch}>
        {children}
      </AuthDispatchContext.Provider>
    </AuthStateContext.Provider>
  );
};
```

定義したプロバイダーをアプリに適用します。

```tsx
const App: React.FunctionComponent = () => {
  return (
    <AuthProvider>
      <NavigationContainer>
        ...
      </NavigationContainer>
    </AuthProvider>
  );
};
```

ここまでで状態管理をするためのプロバイダーが準備できました。
子コンポーネントで`useAuthState`と`useAuthDispatch`が扱えるようになりました。

これで準備は完了です。


# 認証状態による表示画面の出し分け

認証処理を実装します。

```tsx
const Stack = createStackNavigator();
const StackNavigator = () => {
  const state = useAuthState();

  if (state.status === 'Loading') {
    return <LoadingScreen />;
  }

  return (
    <Stack.Navigator
      headerMode="none"
      screenOptions={{ animationEnabled: false }}
    >
      {state.status === 'Authenticated' ? (
        <Stack.Screen name="Home" component={HomeScreen} />
      ) : (
        <Stack.Screen name="SignIn" component={SignInScreen} />
      )}
    </Stack.Navigator>
  );
};
```

`useAuthSate`でアプリの状態を取得することができます。
上記の例では、状態を元に処理を分けています。

状態が`Loading`の場合（データの取得中など）には、`LoadingScreen`を表示します。
それ以外の状態の時は、`Stack.Navigator`を表示します。

ナビゲーターを表示する際には、状態が`Autheticated`（認証済）の場合は`HomeScreen`を、それ以外の場合は`SignInScreen`を表示するように処理しています。

初期状態は`Unauthenticated`からスタートします。
そのため、`SignInScreen`に遷移します。

では、それぞれの画面はどのようになっているのか、見ていきます。

# 各画面の実装

### ログイン画面

```tsx
const SignInScreen: React.FunctionComponent = () => {
  const dispatch = useAuthDispatch();
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="SignIn"
        onPress={() =>
          dispatch({ type: 'COMPLETE_LOGIN', token: 'dummy-token' })
        }
      />
    </View>
  );
};
```

ここではログインボタンだけを実装しています。
ログインボタンを押した際、アプリに`COMPLETE_LOGIN`の状態をディスパッチします。
※本来は認証チェックをすべてパスした場合に、状態をディスパッチすべきですが今回は省略します。

これにより、状態は`Authenticated`へと変化します。

新しい状態に変化すると、`state`が更新されて、再度レンダリングが行われます。
`status`は`Authenticated`になっているため、今度は`HomeScreen`にナビゲーションされます。

### ホーム画面

```tsx
const HomeScreen: React.FunctionComponent = () => {
  const dispatch = useAuthDispatch();
  return (
    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
      <Button
        title="SignOut"
        onPress={() => dispatch({ type: 'COMPLETE_LOGOUT' })}
      />
    </View>
  );
};
```

ここではログアウトボタンだけを実装しています。
ボタンを押下すると、ログアウトした状態（`COMPLETE_LOGOUT`）がディスパッチされます。

これにより、再びログインページへと遷移します。

# おわりに
いかがだったでしょうか。
今回は複雑な処理は省略しましたが、状態で`Stack.Screen`を制御することで許可されていないスクリーンに遷移することを防ぐことができます。

また、サーバーとの通信中はローディング画面を表示する、など柔軟に表示内容を変更することができます。

React Navigation で認証ありのアプリを作成する場合、この認証処理も選択肢としてはありかもしれません。


もし記事に関して何かありましたら、ぜひコメントをよろしくお願いします。