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




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