11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-11-12

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

このテンプレートは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 @types/redux-logger
pod install

Firebaseの設定

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

ディレクトリを作る

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

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

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


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

11
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?