Help us understand the problem. What is going on with this article?

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

このテンプレートはReact Navigation公式サイトのユーザー認証のページがベースになっていて、そこに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
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(エントリーポイント)

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

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

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が必要になってきます。画像を見ながら、必要なものを記述していってください。

CoachingChatApp_–_Authentication_–_Firebase_console.png
CoachingChatApp_–_概要_–_Firebase_console-2.png
CoachingChatApp_–_設定_–_Firebase_console-2.png
CoachingChatApp_–_設定_–_Firebase_console.png

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

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

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

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

Auth.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

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

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

pages

Main.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

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

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

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) => {

  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

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 '../configs/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

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away