TypeScript + React Native + React Navigation v4 + Redux + Firebaseのユーザー認証のテンプレを作った

**こちらはReact Navigation v4についての記事です。 新規で作る場合は、最新のバージョンをお使いください。**

このテンプレートはReact Navigation公式サイトの[ユーザー認証のページ](https://reactnavigation.org/docs/en/auth-flow.html)がベースになっていて、そこにfirebaseとreduxを組み込んだ感じになります。

ついでにクラスコンポーネントから関数コンポーネントへの変更もしています。

## 準備

テンプレートの作成

```
react-native init MyApp --template react-native-template-typescript
```

## インストール

react-native-gesture-handler、react-native-reanimated、react-native-screensは直接使っているわけではありませんが、react-navigationが依存しているようなのでインストールします。

```
yarn add firebase \
react-native-gesture-handler \
react-native-reanimated \
react-native-screens \
react-navigation \
react-navigation-redux-helpers \
react-redux \
redux \
styled-components \
typescript-fsa \
typescript-fsa-reducers \
redux-thunk \
redux-logger \
@react-native-community/async-storage
```

```
yarn add -D @types/react-redux @types/styled-components @types/redux-logger
```

```
pod install
```

## Firebaseの設定

今回はiOSだけ対応するので「アプリを追加」でiOSアプリを追加します。（Firebaseがとても親切なので順に沿って進めてください）

https://firebase.google.com/

## ディレクトリを作る

今回は以下のようなディレクトリ構成ですが、好みに合わせて変更してもらえればいいと思います。

```
index.tsx
src
├── App.tsx
├── actions
│   └── UserAction.ts
├── components
│   └── index.tsx
├── models
│   └── firebase.ts
├── navigations
│   ├── App.ts
│   ├── Auth.ts
│   └── AuthLoading.ts
├── pages
│   ├── App
│   │   └── Main.tsx
│   ├── Auth
│   │   ├── Signin.tsx
│   │   └── Signup.tsx
│   └── AuthLoading
│       └── AuthLoading.tsx
└── reducers
    └── UserReducer.ts
```

一つずつ作るのは、面倒なので準備したコマンドでサクっと作ります。

```
mkdir -p src \
src/actions \
src/components \
src/models \
src/navigations \
src/pages \
src/pages/App \
src/pages/Auth \
src/pages/AuthLoading \
src/reducers
```

```
touch src/App.tsx \
src/actions/UserAction.ts \
src/components/index.tsx \
src/models/firebase.ts \
src/navigations/index.ts \
src/navigations/App.ts \
src/navigations/Auth.ts \
src/navigations/AuthLoading.ts \
src/pages/App/Main.tsx \
src/pages/Auth/Signin.tsx \
src/pages/Auth/Signup.tsx \
src/pages/AuthLoading/AuthLoading.tsx \
src/reducers/UserReducer.ts
```

## index.tsx(エントリーポイント)
```tsx

import { AppRegistry } from 'react-native'
import { App } from './src/App'
import { name as appName } from './app.json'

AppRegistry.registerComponent(appName, () => App)
```

## App.tsx

react-navigationの公式のredux-helpersのREADMEを参考にしています。
https://github.com/react-navigation/redux-helpers

```tsx
import React from 'react'
import { NavigationState } from 'react-navigation'
import { createReduxContainer, createReactNavigationReduxMiddleware, createNavigationReducer } from 'react-navigation-redux-helpers'
import { createStore, applyMiddleware, combineReducers } from 'redux'
import { Provider, connect} from 'react-redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { AppNavigator } from './navigations'
import { UserReducer, UserState } from './reducers/UserReducer'

export type RootState = {
  nav: NavigationState,
  user: UserState,
}

const appReducer = combineReducers({
  nav: createNavigationReducer(AppNavigator),
  user: UserReducer
})

const mapStateToProps = (state: RootState) => {
  return {
    state: state.nav,
  }
}
const AppContainer = createReduxContainer(AppNavigator)
const AppWithNavigationState = connect(mapStateToProps)(AppContainer);

const navMiddleware = createReactNavigationReduxMiddleware(
  (state: RootState) => {
    return state
  }
)
const store = createStore(
  appReducer,
  applyMiddleware(
    navMiddleware,
    thunkMiddleware,
    createLogger()
  ),
)

export const App = () => {
  return (
    <Provider store={store}>
      <AppWithNavigationState />
    </Provider>
  )
}
```

## components

このindex.tsxは最小テンプレートです。新しくコンポーネントを作る時は、これをコピペ、リネームして作ってます。（今回は使用しないので飛ばしても大丈夫です）

### index.tsx
```tsx

import React from 'react'
import { View, Text } from 'react-native'
import { NavigationStackProp } from 'react-navigation-stack'

type NavigateProps = {}
type Props = {
  navigation: NavigationStackProp<NavigateProps>
}

export const Foo: React.FC<Props> = (props) => {
  return (
    <View>
      <Text>Foo</Text>
    </View>
  )
}
```

## models

### firebase.ts

Firebaseを使うにはapiKey, projectId, messagingSenderId, appIDが必要になってきます。画像を見ながら、必要なものを記述していってください。

<img width="981" alt="CoachingChatApp_–_Authentication_–_Firebase_console.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54502/d9f975a7-bf12-128d-0708-9c94b9eb2305.png">
<img width="1096" alt="CoachingChatApp_–_概要_–_Firebase_console-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54502/f8efbb81-a966-eb76-e3eb-f0c6ff79b6ac.png">
<img width="991" alt="CoachingChatApp_–_設定_–_Firebase_console-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54502/ea5f2673-3a37-fca8-f15d-fa2840fc2a8c.png">
<img width="1097" alt="CoachingChatApp_–_設定_–_Firebase_console.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54502/59b8a95d-c51c-e7af-f434-fd3c08b1b32c.png">


```ts
import firebase from 'firebase'
import 'firebase/firestore'

const projectId = 'your project'

export const config = {
  apiKey: 'xxxxxxxxxxxxxxxxxxxxxxx',
  authDomain: `${projectId}.firebaseapp.com`,
  databaseURL: `https://${projectId}.firebaseio.com`,
  projectId,
  storageBucket: `${projectId}.appspot.com`,
  messagingSenderId: 'xxxxxxxxxxxxxxxxxxx',
  appID: "xxxxxxxxxxxxxxxxxxx",
}

firebase.initializeApp(config);

type Auth = (email: string, password: string) => Promise<firebase.auth.UserCredential>

export const signup: Auth = (email, password) => {
  return new Promise((resolve, reject) => {
    firebase.auth().createUserWithEmailAndPassword(email, password)
      .then((user: firebase.auth.UserCredential) => {
        resolve(user)
      })
      .catch((error) => {
        reject(error)
      })
  })
}

export const signin: Auth = (email, password) => {
  return new Promise((resolve, reject) => {
    firebase.auth().signInWithEmailAndPassword(email, password)
      .then((response: firebase.auth.UserCredential) => {
        resolve(response)
      })
      .catch((error) => {
        resolve(error)
      })
  })
}

export default firebase
```

## navigations

### index.ts
```ts

import { createSwitchNavigator } from 'react-navigation'
import { AuthLoadingStack } from './AuthLoading'
import { AuthStack } from './Auth'
import { AppStack } from './App'

export const AppNavigator = createSwitchNavigator({
  AuthLoading: AuthLoadingStack,
  Auth: AuthStack,
  App: AppStack
},
{
  initialRouteName: 'AuthLoading',
})
```

### App.ts

```ts
import { createSwitchNavigator } from 'react-navigation'
import { Main } from '../pages/App/Main'

export const AppStack = createSwitchNavigator({
  Main: {
    screen: Main
  },
})
```

### Auth.ts

```ts
import { createSwitchNavigator } from 'react-navigation'
import { Signin } from '../pages/Auth/Signin'
import { Signup } from '../pages/Auth/Signup'

export const AuthStack = createSwitchNavigator({
  Signin: {
    screen: Signin
  },
  Signup: {
    screen: Signup
  },
})
```

### AuthLoading

```ts
import { createSwitchNavigator } from 'react-navigation'
import { AuthLoading } from '../pages/AuthLoading/AuthLoading'

export const AuthLoadingStack = createSwitchNavigator({
  AuthLoading: {
    screen: AuthLoading
  }
})
```

## pages

### Main.tsx
```tsx

import React, { useCallback } from 'react'
import { View, Text, TouchableOpacity } from 'react-native'
import AsyncStorage from '@react-native-community/async-storage'
import styled from 'styled-components/native'
import { NavigationStackProp } from 'react-navigation-stack'

type NavigateProps = {}
type Props = {
  navigation: NavigationStackProp<NavigateProps>
}

export const Main: React.FC<Props> = (props) => {

  const _handleOnClickSignOut = useCallback(async () => {
    AsyncStorage.removeItem('uid')
    props.navigation.navigate('Auth')
  }, [])

  return (
    <Container>
      <Text>Main</Text>
      <LinkContainer>
        <TouchableOpacity onPress={_handleOnClickSignOut}>
          <Text>ログアウト</Text>
        </TouchableOpacity>
      </LinkContainer>
    </Container>
  )
}

const Container = styled.View`
  flex: 1;
  align-items: center;
  justify-content: center;
`

const LinkContainer = styled.View`
  align-items: center;
  justify-content: center;
`
```

### Signin.tsx

```tsx

import React, { useState, useCallback } from 'react'
import { View, Text, TouchableOpacity, TextInput } from 'react-native'
import AsyncStorage from '@react-native-community/async-storage'
import { signin } from '../../models/firebase' 
import styled from 'styled-components/native'
import { NavigationStackProp } from 'react-navigation-stack'

type NavigateProps = {}
type Props = {
  navigation: NavigationStackProp<NavigateProps>
}

export const Signin: React.FC<Props> = (props) => {

  const [email, setEmail] = useState<string>('')
  const [password, setPassword] = useState<string>('')

  const _handleOnSubmit = useCallback(async () => {
    const res: firebase.auth.UserCredential = await signin(email, password)
    if (res.user) {
      AsyncStorage.setItem('uid', res.user.uid)
      props.navigation.navigate('App')
    }
  }, [email, password])

  const _handleOnClickSignUp = useCallback(async () => {
    props.navigation.navigate('Signup')
  }, [])

  return (
      <Container>
        <View>
          <Text>ログイン</Text>
          <View>
            <View>
              <Text>メールアドレス</Text>
              <TextInput
                onChangeText={(value) => setEmail(value)}
                value={email}
                placeholder="メールアドレスを入力してください"
                placeholderTextColor="#aaa"
                autoCompleteType="email"
              />
            </View>
            <View>
              <Text>パスワード</Text>
              <TextInput
                onChangeText={(value) => setPassword(value)}
                value={password}
                placeholder="****"
                placeholderTextColor="#aaa"
                autoCompleteType="password"
              />
            </View>
          </View>
          <View>
            <Button onPress={_handleOnSubmit}>送信</Button>
          </View>
        </View>
        <LinkContainer>
          <TouchableOpacity onPress={_handleOnClickSignUp}>
            <Text>新規登録</Text>
          </TouchableOpacity>
        </LinkContainer>
      </Container>
  )
}

const Container = styled.View`
  flex: 1;
  align-items: center;
  justify-content: center;
`

const LinkContainer = styled.View`
  align-items: center;
  justify-content: center;
`
```

### Signup.tsx

```tsx
import React, { useState, useCallback } from 'react'
import firebase from 'firebase'
import { View, Text, TouchableOpacity, TextInput } from 'react-native'
import AsyncStorage from '@react-native-community/async-storage'
import { signup } from '../../models/firebase'
import styled from 'styled-components/native'
import { NavigationStackProp } from 'react-navigation-stack'

type NavigateProps = {}
type Props = {
  navigation: NavigationStackProp<NavigateProps>
}

export const Signup: React.FC<Props> = (props) => {

  const [email, setEmail] = useState<string>('')
  const [password, setPassword] = useState<string>('')

  const _handleOnSignIn = useCallback(async () => {
    const res: firebase.auth.UserCredential = await signup(email, password)
    if (res.user) {
      AsyncStorage.setItem('uid', res.user.uid)
      props.navigation.navigate('App')
    }
  }, [email, password])


  const _handleOnClickSignIn = useCallback(async () => {
    props.navigation.navigate('Signin')
  }, [])

  return (
      <Container>
        <View>
          <Text>新規登録</Text>
          <View>
            <View>
              <Text>メールアドレス</Text>
              <TextInput
                onChangeText={(value) => setEmail(value)}
                value={email}
                placeholder="メールアドレスを入力してください"
                placeholderTextColor="#aaa"
                autoCompleteType="email"
              />
            </View>
            <View>
              <Text>パスワード</Text>
              <TextInput
                onChangeText={(value) => setPassword(value)}
                value={password}
                placeholder="****"
                placeholderTextColor="#aaa"
                autoCompleteType="password"
              />
            </View>
          </View>
          <View>
            <Button onPress={_handleOnSignIn}>送信</Button>
          </View>
        </View>
        <LinkContainer>
          <TouchableOpacity onPress={_handleOnClickSignIn}>
            <Text>ログイン</Text>
          </TouchableOpacity>
        </LinkContainer>
      </Container>
  )
}

const Container = styled.View`
  flex: 1;
  align-items: center;
  justify-content: center;
`

const LinkContainer = styled.View`
  align-items: center;
  justify-content: center;
`
```

### AuthLoading.tsx
```tsx

import React, { useEffect } from 'react'
import { View, Text } from 'react-native'
import AsyncStorage from '@react-native-community/async-storage'
import { NavigationStackProp } from 'react-navigation-stack'
import { useDispatch } from 'react-redux'
import { getUser } from '../../actions/UserAction'

type NavigateProps = {}
type Props = {
  navigation: NavigationStackProp<NavigateProps>
}

export const AuthLoading: React.FC<Props> = (props) => {
  const dispatch = useDispatch()
  useEffect(() => {
    AsyncStorage.getItem('uid').then((uid) => {
      if (uid) {
        dispatch(getUser())
        props.navigation.navigate('App')
      } else {
        props.navigation.navigate('Auth')
      }
    }).catch((error) => {
      console.error(error)
      props.navigation.navigate('Auth')
    })
  }, [])

  return (
    <View>
      <Text>AuthLoading</Text>
    </View>
  )
}
```

## actions

### UserAction.ts

```ts
import { Dispatch } from 'redux'
import actionCreatorFactory from 'typescript-fsa'
import { fetchUser } from '../models/UserModel'
import { UserState } from '../reducers/UserReducer'
import { RootState } from '../App'
import firebase from '../models/firebase'

const actionCreator = actionCreatorFactory()

export const setUser = actionCreator.async<null, UserState>('setUser')

export function getUser() {
  const user = firebase.auth().currentUser
  return async (dispatch: Dispatch, getState: () => RootState) => {
    if (user) {
      dispatch(setUser.done({ result: user.displayName || '' , params: null}))
    }
  }
}

const actions = {
  getUser,
  setUser
}

export default actions
```

## reducers
### UserReducer.ts
```ts

import { reducerWithInitialState } from 'typescript-fsa-reducers'
import actions from '../actions/UserAction'

export type UserState = {
  name: string
  organization: string
  introduction: string
}

export const UserInitState = {
  name: '',
  organization: '',
  introduction: ''
}

export const UserReducer = reducerWithInitialState(UserInitState)
  .case(actions.setUser.done, (state, payload) => {
    return Object.assign({}, state, payload.result)
  })
```

## まとめ

仕組みとしては、アプリが立ち上がったらまずAuthLoadingというコンポーネントが呼ばれます。
AuthLoadingでAsyncStorageが`uid`を持っていれば、ユーザー名をReduxにセットし、Main.tsに遷移します。

これは自分用のテンプレートで、現在動いているものから余分なものを削ぎ落として書きました。
なのでもしかしたら、コピペしただけだとエラーが出てしまうかもしれません。（未確認）



## 参考
https://qiita.com/F_PEI/items/64b54f9075805dc0e312
https://reactnavigation.org/docs/en/auth-flow.html