Last active
April 2, 2017 09:55
-
-
Save fumiyasac/25d916d3b8a09ae67f8a260c34ba2bbb to your computer and use it in GitHub Desktop.
ReactNative事始めから簡単なサンプルを読み解くまでの実践記録ノート ref: http://qiita.com/fumiyasac@github/items/71b8ff88d96289d43593
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* アルバム一覧用のコンポーネント | |
*/ | |
import React, { Component } from 'react'; | |
import { ScrollView } from 'react-native'; | |
//HTTP通信用のライブラリ'axios'のインポート宣言 | |
import axios from 'axios'; | |
//アルバム詳細用の共通コンポーネントのインポート宣言 | |
import AlbumDetail from './AlbumDetail'; | |
//コンポーネントの内容を定義する ※ ClassComponent | |
class AlbumList extends Component { | |
//ステートの初期化を行う | |
state = { albums: [] }; | |
//コンポーネントの内容がMountされる前に行う処理 | |
componentWillMount() { | |
//iTunesStoreのAPIよりデータを取得する(axiosを利用) → レスポンスをステートに格納する | |
//補足:APIは下記のURLで使用しているのものと同じになります。 | |
//※参考URL http://qiita.com/fumiyasac@github/items/04c66743a3c829d39b1f | |
axios.get('https://immense-journey-38002.herokuapp.com/articles.json') | |
.then(response => this.setState({ albums: response.data.article.contents })); | |
} | |
//アルバムデータのレンダリングを行う | |
renderAlbums() { | |
//stateに格納されたalbumの個数ぶん<AlbumDetail />の要素を作成する | |
//取得データ:response.data.article.contentsは下記のような形でalbumsオブジェクト内に入る | |
//→ 形式としては、[Object, Object, ...] | |
return this.state.albums.map(album => | |
<AlbumDetail key={album.title} album={album} /> | |
); | |
} | |
//コンポーネントの内容をレンダリングする | |
render() { | |
//データ取得確認用のデバッグログ | |
//console.log(this.state.albums); | |
//表示する要素を返す | |
return ( | |
<ScrollView> | |
{this.renderAlbums()} | |
</ScrollView> | |
); | |
} | |
} | |
//インポート可能にする宣言 | |
export default AlbumList; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* アプリケーション構築用のファイル | |
* | |
* (参考) Reduxの基本的な用語や役割について | |
* ReactとReduxちょっと勉強したときのメモ | |
* http://qiita.com/mgoldchild/items/5be49ea49ebc2e4d9c55#_reference-f1dd704690278d098790 | |
*/ | |
import React, { Component } from 'react'; | |
//React-Reduxのインポート宣言 | |
// → ProviderタグでラップすることでReactコンポーネント内でStoreにアクセスできるようにする | |
// (参考1) React+Redux入門 | |
// http://qiita.com/erukiti/items/e16aa13ad81d5938374e | |
// (参考2) React-Redux をわかりやすく解説しつつ実践的に一部実装してみる | |
// http://ma3tk.hateblo.jp/entry/2016/06/20/182232 | |
import { Provider } from 'react-redux'; | |
//createStore, applyMiddlewareのインポート宣言 | |
// → applyMiddlewareを使うことでdispatch関数をラップしactionがreducerに到達する前にmiddlewareがキャッチできるようにする | |
// (参考1) reduxのcomposeとapplyMiddlewareとenhancer | |
// http://qiita.com/pirosikick/items/d7f9e5e197a2e8aad62f | |
// (参考2) Redux基礎:Middleware編 | |
// http://qiita.com/yasuhiro-okada-aktsk/items/1fe3df267a6034d970c1 | |
// (参考3) ReduxのMiddlewareについて理解したいマン | |
// https://hogehuga.com/post-1123/ | |
import { createStore, applyMiddleware } from 'redux'; | |
//redux-thunkのインポート宣言 | |
// → 非同期処理でアクションを起こすような関数をdispatchに渡せるようにする | |
// (非同期処理に関する参考)react+reduxで非同期処理を含むtodoアプリを作ってみる | |
// http://qiita.com/halhide/items/a45c7a1d5f949596e17d | |
// (参考1) redux-thunkとは? | |
// http://qiita.com/koichirokamoto/items/18f184247ca349cc03a8 | |
// (参考2) Reduxの非同期通信についての(個人的な)整理メモ | |
// http://qiita.com/kmszk/items/c530c33fe5ffdc7a36da | |
import ReduxThunk from 'redux-thunk'; | |
//reducerのインポート宣言 | |
// → ざっくり言えば状態変化を起こすための具体的な処理の寄せ集め | |
// (参考) Redux基礎:Reducer編 | |
// http://qiita.com/yasuhiro-okada-aktsk/items/9d9025cb58ffba35f864 | |
import reducers from './reducers'; | |
//firebaseのインポート宣言を行う | |
import firebase from 'firebase'; | |
//Routerコンポーネントのインポート宣言 | |
import Router from './router'; | |
//アプリの画面の組み立て | |
class App extends Component { | |
//コンポーネントの内容がMountされる前に行う処理 | |
componentWillMount() { | |
//firebaseのセッティング情報を記載する | |
//※ API情報に関してFirebaseコンソールを取得 → Authentication → 「ログイン方法」でメール/パスワードを有効にする | |
const config = { | |
apiKey: "XXX", | |
authDomain: "XXX", | |
databaseURL: "XXX", | |
storageBucket: "XXX", | |
messagingSenderId: "XXX" | |
}; | |
//firebaseを適用する | |
firebase.initializeApp(config); | |
} | |
//見た目データのレンダリングを行う | |
render() { | |
//Redux本来のdispatch処理が実行される前にMiddlewareの処理を実行する | |
//※ 非同期処理でアクションを起こすような関数をdispatchに渡せるようにするReduxThunkを仕込む形にする | |
const store = createStore(reducers, {}, applyMiddleware(ReduxThunk)); | |
//アプリの画面の組み立て | |
return ( | |
<Provider store={store}> | |
<Router /> | |
</Provider> | |
); | |
} | |
} | |
//アプリの画面本体となるこのファイルのエクスポート宣言 | |
export default App; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 入力された従業員データを格納するステートを更新するためのアクション群 | |
* → 各々のアクションが実行されると対応するReducersに定義されたステート更新処理を実行する | |
*/ | |
//firebaseライブラリのインポート宣言 | |
import firebase from 'firebase'; | |
//react-native-router-fluxライブラリのインポート宣言 | |
// (ライブラリ概要)このライブラリのGithubリポジトリ | |
// https://github.com/aksonov/react-native-router-flux | |
// (参考1) React Native Routing について | |
// http://twins-tech.hatenablog.com/entry/2016/06/05/101916 | |
// (参考2) React Native Router Fluxを使ってみませんか・・・? | |
// http://qiita.com/YutamaKotaro/items/ab52b6ba664d88a87bd9 | |
// (参考3) react-native-router-fluxをいい感じに使う3つの方法 | |
// http://web-salad.hateblo.jp/entry/2016/12/04/090000 | |
import { Actions } from 'react-native-router-flux'; | |
//従業員データ関連のアクションタイプ定義のインポート宣言 | |
import { EMPLOYEE_UPDATE, EMPLOYEE_CREATE, EMPLOYEES_FETCH_SUCCESS, EMPLOYEE_SAVE_SUCCESS, EMPLOYEE_DELETE_SUCCESS, EMPLOYEE_REFRESH } from './types'; | |
/** | |
* データ処理の流れとしては、 | |
* (工程1) データに関わる処理の場合はFirebaseに実データを登録・変更・削除の処理を行う | |
* (工程2) firebaseのメソッド処理のコールバック内にステートに関する処理を行う | |
* というイメージの流れ方になります。 | |
*/ | |
//従業員入力前にステートの値を初期化するメソッド | |
export const employeeRefresh = () => { | |
//ステートの中身を初期化するアクションを実行する | |
return { type: EMPLOYEE_REFRESH }; | |
}; | |
//従業員入力時にステートの値を更新するメソッド | |
export const employeeUpdate = ({ prop, value }) => { | |
//ステート更新アクションを実行する | |
// ※ onChangeText={value => this.props.employeeUpdate({ prop: 'phone', value })} | |
// → EmployeeForm.jsでprop名と入力された値をActionに仕込む形にする | |
return { type: EMPLOYEE_UPDATE, payload: { prop, value } }; | |
}; | |
//従業員データを新規に1件追加するメソッド | |
export const employeeCreate = ({ name, phone, shift }) => { | |
//現在認証されているユーザーを取得する | |
const { currentUser } = firebase.auth(); | |
//データに関する処理を実行する(非同期での実行) | |
return (dispatch) => { | |
//firebaseのDatabaseへアクセスを行い新規データを登録する(新規追加時に一意なIDを作成される) | |
// (データの持ち方や参照方法は公式ドキュメントを参考にしてみてください) | |
firebase.database().ref(`/users/${currentUser.uid}/employees`) | |
.push({ name, phone, shift }) | |
.then(() => { | |
//ステートの更新アクションを実行(※従業員データの追加) | |
dispatch({ type: EMPLOYEE_CREATE }); | |
//従業員一覧画面へ遷移する | |
Actions.employeeList({ type: 'reset' }); | |
}); | |
}; | |
}; | |
//従業員データの一覧を取得するメソッド | |
export const employeesFetch = () => { | |
//現在認証されているユーザーを取得する | |
const { currentUser } = firebase.auth(); | |
//データに関する処理を実行する(非同期での実行) | |
return (dispatch) => { | |
//firebaseのDatabaseへアクセスを行いユーザーに紐づく従業員データを全て取得する | |
// (データの持ち方や参照方法は公式ドキュメントを参考にしてみてください) | |
// 基本的にはiOSのfirebaseからのfetch処理同様にsnapshotを受け取ったタイミングでの処理内容をクロージャー内に記載する | |
firebase.database().ref(`/users/${currentUser.uid}/employees`) | |
.on('value', snapshot => { | |
//ステートの更新アクションを実行(※従業員一覧の取得成功) | |
dispatch({ type: EMPLOYEES_FETCH_SUCCESS, payload: snapshot.val() }); | |
}); | |
}; | |
}; | |
//該当IDの従業員データを1件更新するメソッド(引数にuidがある) | |
export const employeeSave = ({ name, phone, shift, uid }) => { | |
//現在認証されているユーザーを取得する | |
const { currentUser } = firebase.auth(); | |
//データに関する処理を実行する | |
return (dispatch) => { | |
//firebaseのDatabaseへアクセスを行い一意なID(uidがそれにあたる)に該当するデータを更新する | |
firebase.database().ref(`/users/${currentUser.uid}/employees/${uid}`) | |
.set({ name, phone, shift }) | |
.then(() => { | |
//ステートの更新アクションを実行(※従業員データの更新) | |
dispatch({ type: EMPLOYEE_SAVE_SUCCESS }); | |
//従業員一覧画面へ遷移する | |
Actions.employeeList({ type: 'reset' }); | |
}); | |
}; | |
}; | |
//該当IDの従業員データを1件削除するメソッド(引数にuidがある) | |
export const employeeDelete = ({ uid }) => { | |
//現在認証されているユーザーを取得する | |
const { currentUser } = firebase.auth(); | |
//データに関する処理を実行する(非同期での実行) | |
return (dispatch) => { | |
//firebaseのDatabaseへアクセスを行い一意なID(uidがそれにあたる)に該当するデータを削除する | |
firebase.database().ref(`/users/${currentUser.uid}/employees/${uid}`) | |
.remove() | |
.then(() => { | |
//ステートの更新アクションを実行(※従業員データの削除) | |
dispatch({ type: EMPLOYEE_DELETE_SUCCESS }); | |
//従業員一覧画面へ遷移する | |
Actions.employeeList({ type: 'reset' }); | |
}); | |
}; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 顧客情報新規追加用部分のコンポーネント | |
*/ | |
import React, { Component } from 'react'; | |
//connectのインポート宣言を行う | |
// → connectを用いてstoreをpropで読めるようにする | |
// 参考:[redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
import { connect } from 'react-redux'; | |
//ActionCreator(Actionの寄せ集め)のインポート宣言(this.props.この中に定義したメソッド名の形で実行) | |
import { employeeUpdate, employeeCreate, employeeRefresh } from '../actions'; | |
//共通設定した部品のインポート宣言 | |
import { GridArea, GridSection, Button } from './common'; | |
//自作コンポーネント:EmployeeFormのインポート宣言 | |
import EmployeeForm from './EmployeeForm'; | |
//コンポーネントの内容を定義する ※ ClassComponent | |
class EmployeeCreate extends Component { | |
//コンポーネントの内容がMountされる前に行う処理 | |
componentWillMount() { | |
//stateの中を一旦リフレッシュする ※編集画面から特に処理を行わずにバックした際の考慮 | |
this.props.employeeRefresh(); | |
} | |
//Createボタン押下時の処理 | |
onButtonPress() { | |
//取得したthis.propsの値をそれぞれの値に分割する | |
const { name, phone, shift } = this.props; | |
//新規追加用のデータを受け取り新規に1件データを追加する | |
this.props.employeeCreate({ name, phone, shift: shift || 'Monday' }); | |
} | |
//見た目データのレンダリングを行う | |
render() { | |
return ( | |
<GridArea> | |
{ /* 1. 従業員フォームのコンポーネントを表示する */ } | |
<EmployeeForm {...this.props} /> | |
{ /* 2. データの新規追加用のボタン表示 */ } | |
<GridSection> | |
<Button onPress={this.onButtonPress.bind(this)}> | |
Create | |
</Button> | |
</GridSection> | |
</GridArea> | |
); | |
} | |
} | |
//ステートから値を取得してthis.propsにセットする | |
// → 内容は「reducers/index.js」を参照 | |
// ※ Reducerにあるものを再度詰め直しを行うイメージ | |
const mapStateToProps = (state) => { | |
//引数で受け取った認証データを変数に分解する | |
const { name, phone, shift } = state.employeeForm; | |
//分解したそれぞれの値をオブジェクトにして返却する | |
return { name, phone, shift }; | |
}; | |
//インポート可能にする宣言 | |
// ※書き方メモ:export default connect(mapStateToProps, mapDispatchToProps)(Class)の形で記述する | |
// | |
// 引数: | |
// mapStateToProps:globalなstateから利用する値をとってきてthis.propsにセットする | |
// mapDispatchToProps:this.method.actionHoge()を呼ぶとstore.dispatch()が呼ばれる → アクションを定義している場合にはそのアクションメソッドを設定 | |
// | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
export default connect(mapStateToProps, { employeeUpdate, employeeCreate, employeeRefresh })(EmployeeCreate); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 顧客情報編集用部分のコンポーネント | |
*/ | |
import React, { Component } from 'react'; | |
//オブジェクトを配列に変換するのに便利なライブラリ「lodash」のインポート宣言 | |
// → underscore.jsとほぼ同様の機能を提供してくれるもの? | |
// (公式ドキュメント) | |
// https://lodash.com/docs | |
// 参考:JavaScriptで関数型プログラミングを強力に後押しするUnderscore.jsのおすすめメソッド12選(lodashもあるよ) | |
// http://qiita.com/takeharu/items/7d4ead780710c627172e | |
// 参考:lodashでよく使う関数まとめ | |
// http://matsukaz.hatenablog.com/entry/2014/04/09/082410 | |
import _ from 'lodash'; | |
//connectのインポート宣言を行う | |
// → connectを用いてstoreをpropで読めるようにする | |
// 参考:[redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
import { connect } from 'react-redux'; | |
//電話やEメールを開くためのライブラリ「react-native-communications」のインポート宣言 | |
import Communications from 'react-native-communications'; | |
//共通設定した部品のインポート宣言 | |
import { GridArea, GridSection, Button, Confirm } from './common'; | |
//自作コンポーネント:EmployeeFormのインポート宣言 | |
import EmployeeForm from './EmployeeForm'; | |
//ActionCreator(Actionの寄せ集め)のインポート宣言(this.props.この中に定義したメソッド名の形で実行) | |
import { employeeUpdate, employeeSave, employeeDelete } from '../actions'; | |
//コンポーネントの内容を定義する ※ ClassComponent | |
class EmployeeEdit extends Component { | |
//このコンポーネント内のステート ※このステートはモーダルのコントロールをするために使用する | |
state = { showModal: false }; | |
//コンポーネントの内容がMountされる前に行う処理 | |
componentWillMount() { | |
//this.props.employee(Storeから取ってきたもの)を再度マッピングをし直してステートを更新する ※値とキーが反対なので注意する | |
_.forEach(this.props.employee, (value, prop) => { | |
this.props.employeeUpdate({ prop, value }); | |
}); | |
} | |
//「Save Changes」ボタン押下時の処理 | |
onButtonPress() { | |
//取得したthis.propsの値をそれぞれの値に分割する(入力・選択対象のデータを取得する) | |
const { name, phone, shift } = this.props; | |
//uidがキーとなる既存1件のデータを更新する | |
this.props.employeeSave({ name, phone, shift, uid: this.props.employee.uid }); | |
} | |
//シフト更新用のボタンを押下した際の処理 | |
onTextPress() { | |
//取得したthis.propsの値をそれぞれの値に分割する | |
const { phone, shift } = this.props; | |
//ボタンを押下すると電話がかかるようにする | |
Communications.text(phone, `Your upcoming shift is on ${shift}`); | |
} | |
//onAccept属性に設定した関数が発火した際の処理 | |
onAccept() { | |
//取得したthis.propsの値をそれぞれの値に分割する(uidだけを取得する) | |
const { uid } = this.props.employee; | |
//uidがキーとなる既存1件のデータを削除する | |
this.props.employeeDelete({ uid }); | |
} | |
//onDecline属性に設定した関数が発火した際の処理 | |
onDecline() { | |
//このコンポーネントのstateをfalseに戻す | |
this.setState({ showModal: false }); | |
} | |
//見た目データのレンダリングを行う | |
render() { | |
return ( | |
<GridArea> | |
{ /* 1. 従業員フォームのコンポーネント */ } | |
<EmployeeForm /> | |
{ /* 2. データの更新用のボタン */ } | |
<GridSection> | |
<Button onPress={this.onButtonPress.bind(this)}> | |
Save Changes | |
</Button> | |
</GridSection> | |
{ | |
/** | |
* 3. シフト部分のボタン → Communicationsを使用して電話をかけられるようにする | |
*/ | |
} | |
<GridSection> | |
<Button onPress={this.onTextPress.bind(this)}> | |
Text Schedule | |
</Button> | |
</GridSection> | |
{ | |
/** | |
* 4. モーダル表示用のトリガーとなるボタン → ※デフォルト値がfalseなのでtrueにする → そうすることで削除用のモーダルが表示される形になる | |
*/ | |
} | |
<GridSection> | |
<Button onPress={() => this.setState({ showModal: !this.state.showModal })}> | |
Fire Employee | |
</Button> | |
</GridSection> | |
{ | |
/** | |
* 5. モーダル表示 | |
* ※this.stateと連動してモーダルの表示・非表示が決定する | |
*/ | |
} | |
<Confirm visible={this.state.showModal} onAccept={this.onAccept.bind(this)} onDecline={this.onDecline.bind(this)}> | |
Are you sure you want to delete this? | |
</Confirm> | |
</GridArea> | |
); | |
} | |
} | |
//ステートから値を取得してthis.propsにセットする | |
// → 内容は「reducers/index.js」を参照 | |
// ※ Reducerにあるものを再度詰め直しを行うイメージ | |
const mapStateToProps = (state) => { | |
//引数で受け取った認証データを変数に分解する | |
const { name, phone, shift } = state.employeeForm; | |
//分解したそれぞれの値をオブジェクトにして返却する | |
return { name, phone, shift }; | |
}; | |
//インポート可能にする宣言 | |
// ※書き方メモ:export default connect(mapStateToProps, mapDispatchToProps)(Class)の形で記述する | |
// | |
// 引数: | |
// mapStateToProps:globalなstateから利用する値をとってきてthis.propsにセットする | |
// mapDispatchToProps:this.method.actionHoge()を呼ぶとstore.dispatch()が呼ばれる → アクションを定義している場合にはそのアクションメソッドを設定 | |
// | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
export default connect(mapStateToProps, { employeeUpdate, employeeSave, employeeDelete })(EmployeeEdit); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 顧客情報入力フォーム用部分のコンポーネント | |
*/ | |
//Firebase側のセキュリティルール | |
/* Realtime Databaseのルールタブ部分に下記のような記載をしておく | |
{ | |
"rules": { | |
"users": { | |
"$uid": { | |
".read": "$uid === auth.uid", | |
".write": "$uid === auth.uid" | |
} | |
} | |
} | |
} | |
*/ | |
import React, { Component } from 'react'; | |
import { View, Text, Picker } from 'react-native'; | |
//connectのインポート宣言を行う | |
// → connectを用いてstoreをpropで読めるようにする | |
// 参考:[redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
import { connect } from 'react-redux'; | |
//ActionCreator(Actionの寄せ集め)のインポート宣言(this.props.この中に定義したメソッド名の形で実行) | |
import { employeeUpdate } from '../actions'; | |
//共通設定した部品のインポート宣言 | |
import { GridSection, Input } from './common'; | |
//コンポーネントの内容を定義する ※ ClassComponent | |
//propsの値を受け取って入力処理が行われたらステートの更新を行う | |
class EmployeeForm extends Component { | |
render() { | |
return ( | |
<View> | |
{ /* 1.名前の入力エリア */ } | |
<GridSection> | |
<Input | |
label="Name" | |
placeholder="Jane" | |
value={this.props.name} | |
onChangeText={value => this.props.employeeUpdate({ prop: 'name', value })} | |
/> | |
</GridSection> | |
{ /* 2.電話番号の入力エリア */ } | |
<GridSection> | |
<Input | |
label="Phone" | |
placeholder="555-555-5555" | |
value={this.props.phone} | |
onChangeText={value => this.props.employeeUpdate({ prop: 'phone', value })} | |
/> | |
</GridSection> | |
{ /* 3.曜日の選択エリア */ } | |
<GridSection style={{ flexDirection: 'column' }}> | |
<Text style={styles.pickerTextStyle}>Shift</Text> | |
<Picker | |
style={styles.pickerContainerStyle} | |
selectedValue={this.props.shift} | |
onValueChange={value => this.props.employeeUpdate({ prop: 'shift', value })} | |
> | |
<Picker.Item label="Monday" value="Monday" /> | |
<Picker.Item label="Tuesday" value="Tuesday" /> | |
<Picker.Item label="Wednesday" value="Wednesday" /> | |
<Picker.Item label="Thursday" value="Thursday" /> | |
<Picker.Item label="Friday" value="Friday" /> | |
<Picker.Item label="Saturday" value="Saturday" /> | |
<Picker.Item label="Sunday" value="Sunday" /> | |
</Picker> | |
</GridSection> | |
</View> | |
); | |
} | |
} | |
//このコンポーネントのStyle定義 | |
const styles = { | |
pickerContainerStyle: { | |
flex: 1 | |
}, | |
pickerTextStyle: { | |
fontSize: 14, | |
paddingTop: 8, | |
paddingLeft: 20 | |
} | |
}; | |
//ステートから値を取得してthis.propsにセットする | |
// → 内容は「reducers/index.js」を参照 | |
// ※ Reducerにあるものを再度詰め直しを行うイメージ | |
const mapStateToProps = (state) => { | |
//引数で受け取った認証データを変数に分解する | |
const { name, phone, shift } = state.employeeForm; | |
//分解したそれぞれの値をオブジェクトにして返却する | |
return { name, phone, shift }; | |
}; | |
//インポート可能にする宣言 | |
// ※書き方メモ:export default connect(mapStateToProps, mapDispatchToProps)(Class)の形で記述する | |
// | |
// 引数: | |
// mapStateToProps:globalなstateから利用する値をとってきてthis.propsにセットする | |
// mapDispatchToProps:this.method.actionHoge()を呼ぶとstore.dispatch()が呼ばれる → アクションを定義している場合にはそのアクションメソッドを設定 | |
// | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
export default connect(mapStateToProps, { employeeUpdate })(EmployeeForm); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 従業員データの追加・変更時の状態ステータスを管理するためのReducer | |
* → 定義されたaction経由で実行されるステートに関する処理に関するロジックを定義する | |
* | |
* ※ Reducerの原則: | |
* (1) 現在のstateオブジェクト(state)を変更せずに新しいステートオブジェクトを作成して返す。 | |
* (2) 受け取るのはステートオブジェクト(state)とアクション(action)の2つ。 | |
* (3) 変更は全てpureな関数で書かれる。 | |
* (4) 受け取ったステートは読み取りだけできる。 | |
* (5) storeからアクション(action)と現在保持しているステートが渡ってくる | |
*/ | |
//従業員データの追加・変更時の状態ステータス関連のアクションタイプ定義のインポート宣言 | |
import { EMPLOYEE_UPDATE, EMPLOYEE_CREATE, EMPLOYEE_SAVE_SUCCESS, EMPLOYEE_DELETE_SUCCESS, EMPLOYEE_REFRESH } from '../actions/types'; | |
//初期状態のステート定義(オブジェクトの形にする) | |
const INITIAL_STATE = { name: '', phone: '', shift: '' }; | |
// JFYI: この辺りのドキュメントを追っかけてもいいかもしれない | |
// → Reduxを動かしながら理解する | |
// http://takayukii.me/post/20160122426 | |
//選択されたケースを元にstateの更新を行うメソッド(アクションのタイプに応じての場合分けがされている) | |
export default (state = INITIAL_STATE, action) => { | |
switch (action.type) { | |
case EMPLOYEE_UPDATE: | |
return { ...state, [action.payload.prop]: action.payload.value }; | |
case EMPLOYEE_CREATE: | |
return INITIAL_STATE; | |
case EMPLOYEE_SAVE_SUCCESS: | |
return INITIAL_STATE; | |
case EMPLOYEE_DELETE_SUCCESS: | |
return INITIAL_STATE; | |
case EMPLOYEE_REFRESH: | |
return INITIAL_STATE; | |
default: | |
return state; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 顧客一覧表示用部分のコンポーネント | |
*/ | |
import React, { Component } from 'react'; | |
import { ListView } from 'react-native'; | |
//オブジェクトを配列に変換するのに便利なライブラリ「lodash」のインポート宣言 | |
// → underscore.jsとほぼ同様の機能を提供してくれるもの? | |
// (公式ドキュメント) | |
// https://lodash.com/docs | |
// 参考:JavaScriptで関数型プログラミングを強力に後押しするUnderscore.jsのおすすめメソッド12選(lodashもあるよ) | |
// http://qiita.com/takeharu/items/7d4ead780710c627172e | |
// 参考:lodashでよく使う関数まとめ | |
// http://matsukaz.hatenablog.com/entry/2014/04/09/082410 | |
import _ from 'lodash'; | |
//connectのインポート宣言を行う | |
// → connectを用いてstoreをpropで読めるようにする | |
// 参考:[redux] Presentational / Container componentの分離 - react-redux.connect()のつかいかた | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
import { connect } from 'react-redux'; | |
//ActionCreator(Actionの寄せ集め)のインポート宣言(this.props.この中に定義したメソッド名の形で実行) | |
import { employeesFetch } from '../actions'; | |
//自作コンポーネント:ListItemのインポート宣言 | |
import ListItem from './ListItem'; | |
//コンポーネントの内容を定義する ※ ClassComponent | |
class EmployeeList extends Component { | |
//コンポーネントの内容がMountされる前に行う処理 | |
componentWillMount() { | |
//ステートから値を取得してthis.propsにセットする処理を実行する | |
this.props.employeesFetch(); | |
//propsから取得できた値をListViewのデータソースへ格納する | |
this.createDataSource(this.props); | |
} | |
//処理の過程の中でpropsを再度受け取った際に行う処理 | |
componentWillReceiveProps(nextProps) { | |
//コメント: | |
// nextProps are the next set of props that this component | |
// will be rendered with | |
// this.props is still the old set of props | |
// → 要は再度値が変更された場合にListViewを更新してもthis.propsの値は変化していないのでこのような形にしている | |
//propsから取得できた値をListViewのデータソースへ格納する | |
this.createDataSource(nextProps); | |
} | |
//データソース部分の定義をする | |
//初期化済みのDataSourceを準備する | |
//rowHasChanged:各データの同一性を検証する(r1とr2を比較して違うものかどうかを返す) | |
createDataSource({ employees }) { | |
const ds = new ListView.DataSource({ | |
rowHasChanged: (r1, r2) => r1 !== r2 | |
}); | |
//dataSourceに値を入れる | |
//cloneWithRows:DataSourceを複製して引数で与えられた値を追加する | |
this.dataSource = ds.cloneWithRows(employees); | |
} | |
//リストになっている出前データの一覧表示用の部品(<ListItem>)の設定を行う | |
renderRow(employee) { | |
return <ListItem employee={employee} />; | |
} | |
//見た目データのレンダリングを行う | |
render() { | |
//見た目に関する処理をする | |
// → 表示の際にはこの2つを設定: | |
// (1) <ListView>のpropsに表示させたいデータを指定するdataSource | |
// (2) データの表示方法を指定するrenderRow ※renderRow(delivery)の引数は設定される | |
return ( | |
<ListView enableEmptySections dataSource={this.dataSource} renderRow={this.renderRow} /> | |
); | |
} | |
} | |
//ステートから値を取得してthis.propsにセットする | |
// → 内容は「reducers/index.js」を参照 | |
// ※ Reducerにあるものを再度詰め直しを行うイメージ | |
const mapStateToProps = state => { | |
//lodashのmapメソッドを用いてステートの値をListView表示用に整形する | |
// (参考) https://lodash.com/docs/4.17.4#map | |
const employees = _.map(state.employees, (val, uid) => { | |
//Objectにそれぞれの値を格納する { name: "ALEX", phone: "03-1234-5678", shift: "Monday", uid: "aS4Xuce-us5Sei5ka" }のような形 | |
return { ...val, uid }; | |
}); | |
//上記で生成したオブジェクトの固まりを返す | |
return { employees }; | |
}; | |
//インポート可能にする宣言 | |
// ※書き方メモ:export default connect(mapStateToProps, mapDispatchToProps)(Class)の形で記述する | |
// | |
// 引数: | |
// mapStateToProps:globalなstateから利用する値をとってきてthis.propsにセットする | |
// mapDispatchToProps:this.method.actionHoge()を呼ぶとstore.dispatch()が呼ばれる → アクションを定義している場合にはそのアクションメソッドを設定 | |
// | |
// http://qiita.com/yuichiroTCY/items/a3ca7d9d415049d02d60 | |
export default connect(mapStateToProps, { employeesFetch })(EmployeeList); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 従業員一覧表示時の状態ステータスを管理するためのReducer | |
* → 定義されたaction経由で実行されるステートに関する処理に関するロジックを定義する | |
* | |
* ※ Reducerの原則: | |
* (1) 現在のstateオブジェクト(state)を変更せずに新しいステートオブジェクトを作成して返す。 | |
* (2) 受け取るのはステートオブジェクト(state)とアクション(action)の2つ。 | |
* (3) 変更は全てpureな関数で書かれる。 | |
* (4) 受け取ったステートは読み取りだけできる。 | |
* (5) storeからアクション(action)と現在保持しているステートが渡ってくる | |
*/ | |
//従業員一覧表示時時の状態ステータス関連のアクションタイプ定義のインポート宣言 | |
import { EMPLOYEES_FETCH_SUCCESS } from '../actions/types'; | |
//初期状態のステート定義(オブジェクトの形にする) | |
const INITIAL_STATE = {}; | |
// JFYI: この辺りのドキュメントを追っかけてもいいかもしれない | |
// → Reduxを動かしながら理解する | |
// http://takayukii.me/post/20160122426 | |
//選択されたケースを元にstateの更新を行うメソッド(アクションのタイプに応じての場合分けがされている) | |
export default (state = INITIAL_STATE, action) => { | |
switch (action.type) { | |
case EMPLOYEES_FETCH_SUCCESS: | |
return action.payload; | |
default: | |
return state; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Node.jsのインストール(事前にHomeBrewをインストールする必要がある) | |
$ brew install node | |
$ brew install watchman | |
//react-native-cliの再インストール(以前に他のバージョンをしていたため下記のコマンドを実行した) | |
$ npm uninstall react-native-cli -g | |
$ npm install npm@latest -g | |
$ npm install react-native-cli -g | |
//react-nativeのインストール | |
$ react-native init ProjectName |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Sample React Native App | |
* https://github.com/facebook/react-native | |
* @flow | |
*/ | |
import React, { Component } from 'react'; | |
import { | |
AppRegistry, | |
Text, | |
View, | |
StyleSheet, | |
TouchableHighlight | |
} from 'react-native'; | |
//minutes-seconds-millisecondsパッケージのインポート | |
import FormatTime from 'minutes-seconds-milliseconds' | |
//ストップウォッチ用の画面部品を設定する | |
var StopWatch = React.createClass({ | |
//Stateの初期化を行う | |
/** | |
* 定義されているStateは下記の通り | |
* | |
* timeElapsed: 現在のストップウォッチの時間表示 | |
* isRunning: 現在のストップウォッチの状態判定フラグ | |
* startTime: 開始時間格納用 | |
* laps: ラップ記録データ格納用配列 | |
*/ | |
getInitialState: function() { | |
return { | |
timeElapsed: null, | |
isRunning: false, | |
startTime: null, | |
laps: [] | |
} | |
}, | |
//見た目のViewを組み立てる | |
render: function() { | |
return ( | |
<View style={styles.container}> | |
<View style={styles.header}> | |
<View style={styles.timerWrapper}> | |
<Text style={styles.timerText}> | |
{FormatTime(this.state.timeElapsed)} | |
</Text> | |
</View> | |
<View style={styles.buttonWrapper}> | |
{this.startStopButton()} | |
{this.lapButton()} | |
</View> | |
</View> | |
<View style={styles.footer}> | |
{this.displayLaps()} | |
</View> | |
</View> | |
) | |
}, | |
//ラップデータ表示用のメソッド(Lapボタン押下時にlapsに格納されたデータを表示する) | |
displayLaps: function() { | |
//lapsに格納されている値を入れた要素を表示する | |
return this.state.laps.map(function(time, index){ | |
return ( | |
<View key={index} style={styles.lapView}> | |
<Text style={styles.lapText}>Lap #{index + 1}</Text> | |
<Text style={styles.lapText}>{FormatTime(time)}</Text> | |
</View> | |
) | |
}); | |
}, | |
//スタートボタンを押下した際のメソッド | |
//(変数:isRunningの状態によってボタンの振る舞いが変わる) | |
//(クリックをするとメソッド:handleStartPressが発火する) | |
startStopButton: function() { | |
//状態によってスタイルが変化するように設定する | |
var setStyle = this.state.isRunning ? styles.stopButtonStyle : styles.startButtonStyle; | |
//ボタン要素を返却して要素を表示する | |
return ( | |
<TouchableHighlight underlayColor="gray" onPress={this.handleStartPress} style={[styles.buttonAbstractStyle, setStyle]}> | |
<Text>{this.state.isRunning ? "Stop" : "Start"}</Text> | |
</TouchableHighlight> | |
) | |
}, | |
//ラップ記録ボタンを押下した際のメソッド | |
//(クリックをするとメソッド:handleLapPressが発火する) | |
lapButton: function() { | |
//ボタン要素を返却して要素を表示する | |
return ( | |
<TouchableHighlight underlayColor="gray" onPress={this.handleLapPress} style={styles.buttonAbstractStyle}> | |
<Text>Lap</Text> | |
</TouchableHighlight> | |
) | |
}, | |
//スタートボタンを押下した際に実行されるメソッド | |
handleStartPress: function() { | |
//タイマーが実行中の際はタイマーをクリアする(isRunningはfalseにする) | |
if (this.state.isRunning) { | |
clearInterval(this.interval); | |
this.setState({ | |
isRunning: false | |
}); | |
return; | |
} | |
//タイマーが実行されていない場合は開始時間をstateへ記録しておく | |
this.setState({ | |
startTime: new Date() | |
}); | |
//0.03秒ごとに、timeElapsed:が更新されてストップウォッチ表示の時間が更新される | |
/** | |
* setIntervalメソッド内の記載はアロー関数で行っている | |
* | |
*(参考)ECMAScript6のアロー関数とPromiseまとめ - JavaScript | |
* http://qiita.com/takeharu/items/c23998d22903e6d3c1d9 | |
*(参考)MDNのアロー関数の解説 | |
* https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/arrow_functions | |
*/ | |
this.interval = setInterval( () => { | |
//timeElapsedに現在時刻と開始時間の差分を記録してisRunningの値をtrueの状態にしておく | |
this.setState({ | |
timeElapsed: new Date() - this.state.startTime, | |
isRunning: true | |
}); | |
}, 30); | |
}, | |
//ラップ記録ボタンを押下した際に実行されるメソッド | |
handleLapPress: function() { | |
//現在の時間をlaps内に記録する(concatメソッドで配列を連結する) | |
//(参考)https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/concat | |
var lap = this.state.timeElapsed; | |
this.setState({ | |
startTime: new Date(), | |
laps: this.state.laps.concat([lap]) | |
}); | |
} | |
}); | |
//画面のスタイルを適用する | |
//(参考)A Complete Guide to Flexbox | |
// https://gibbon.co/c/fcad97d6-c1d0-49a1-a137-2d366fc079f8/a-complete-guide-to-flexbox-csstricks | |
var styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
alignItems: 'stretch' | |
}, | |
header: { | |
flex: 1 | |
}, | |
footer: { | |
flex: 1 | |
}, | |
timerWrapper: { | |
flex: 5, | |
justifyContent: 'center', | |
alignItems: 'center' | |
}, | |
buttonWrapper: { | |
flex: 3, | |
flexDirection: 'row', | |
justifyContent: 'space-around', | |
alignItems: 'center' | |
}, | |
timerText: { | |
fontSize: 60 | |
}, | |
buttonAbstractStyle: { | |
borderWidth: 2, | |
height: 100, | |
width: 100, | |
borderRadius: 50, | |
justifyContent: 'center', | |
alignItems: 'center' | |
}, | |
startButtonStyle: { | |
borderColor: "#00CC00" | |
}, | |
stopButtonStyle: { | |
borderColor: "#CC0000" | |
}, | |
lapView: { | |
flexDirection: 'row', | |
justifyContent: 'space-around' | |
}, | |
lapText: { | |
fontSize: 30 | |
} | |
}); | |
AppRegistry.registerComponent('StopwatchOfReactNative', () => StopWatch); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ログイン用のフォーム部分のコンポーネント | |
*/ | |
import React, { Component } from 'react'; | |
import { Text } from 'react-native'; | |
//firebaseのインポート宣言を行う | |
import firebase from 'firebase'; | |
//LoginFormの作成に必要な自作コンポーネント群のインポート宣言を行う | |
import { Button, Card, CardSection, Input, Spinner } from './common'; | |
//ログイン用フォーム部分のUIの組み立てを行う | |
class LoginForm extends Component { | |
//初期状態のステートと対応する値を定義する | |
state = { email: '', password: '', error: '', loading: false }; | |
//ボタン押下時に実行されるメソッド | |
onButtonPress() { | |
//ステートからメールアドレスとパスワードを取得する | |
const { email, password } = this.state; | |
//ステートの状態を変更する ※loadingをtrueに変更してスピナー表示をする | |
this.setState({ error: '', loading: true }); | |
//firebaseへの認証を行う | |
//サインイン用のfirebaseのメソッドauth().signInWithEmailAndPassword(email, password)を利用する | |
// → then内のログイン認証処理を実行(ログイン処理を実行する) ※エラーの際にはcatch以下の処理を実行する | |
// → ログイン処理に失敗した場合はアカウント作成を行いログイン状態にする ※エラーの際にはcatch以下の処理を実行する | |
// | |
// (参考)今更だけどPromise入門 | |
// http://qiita.com/koki_cheese/items/c559da338a3d307c9d88 | |
firebase.auth().signInWithEmailAndPassword(email, password) | |
.then(this.onLoginSuccess.bind(this)) | |
.catch(() => { | |
firebase.auth().createUserWithEmailAndPassword(email, password) | |
.then(this.onLoginSuccess.bind(this)) | |
.catch(this.onLoginFail.bind(this)); | |
}); | |
} | |
//ログイン処理に失敗した場合に実行されるメソッド | |
onLoginFail() { | |
//ステート内の値を更新する | |
this.setState({ error: '認証に失敗しました。', loading: false }); | |
} | |
//ログイン処理に成功した場合に実行されるメソッド | |
onLoginSuccess() { | |
//ステート内の値を更新する | |
this.setState({ email: '', password: '', error: '', loading: false }); | |
} | |
//現在の実行状態と紐づいたボタンのレンダリングを行うメソッド | |
renderButton() { | |
//状態がアクセスの最中ならばインジケーターを表示する | |
//※ <Spinner>コンポーネントを自作している | |
if (this.state.loading) { | |
return <Spinner size="small" />; | |
} | |
return ( | |
<Button onPress={this.onButtonPress.bind(this)}> | |
ログイン&サインアップ | |
</Button> | |
); | |
} | |
//見た目データのレンダリングを行う | |
render() { | |
return( | |
<Card> | |
{ | |
//1. Eメールアドレスの入力部分 | |
} | |
<CardSection> | |
{ | |
/** | |
* this.state.emailで現在stateに格納されているemailの文字列を取得する | |
* onChangeText内にテキストが変更されたタイミングでstateに格納する値を変更する | |
*/ | |
} | |
<Input | |
placeholder="[email protected]" | |
label="メールアドレス" | |
value={this.state.email} | |
onChangeText={ email => this.setState({ email }) } | |
/> | |
</CardSection> | |
{ | |
//2. パスワードの入力部分 | |
} | |
<CardSection> | |
{ | |
/** | |
* this.state.passwordで現在stateに格納されているpasswordの文字列を取得する | |
* onChangeText内にテキストが変更されたタイミングでstateに格納する値を変更する | |
* パスワードは見えないように'secureTextEntry'の値を設定する | |
*/ | |
} | |
<Input | |
secureTextEntry | |
placeholder="password" | |
label="パスワード" | |
value={this.state.password} | |
onChangeText={ password => this.setState({ password }) } | |
/> | |
</CardSection> | |
{ | |
//3. 認証失敗時のエラーメッセージ表示部分 | |
} | |
<Text style={styles.errorTextStyle}> | |
{this.state.error} | |
</Text> | |
{ | |
//4. 認証を行うボタン部分 | |
} | |
<CardSection> | |
{this.renderButton()} | |
</CardSection> | |
</Card> | |
); | |
} | |
}; | |
//このコンポーネントのStyle定義 | |
const styles = { | |
errorTextStyle: { | |
fontSize: 16, | |
alignSelf: 'center', | |
color: 'red' | |
} | |
}; | |
//ログイン用フォームの実体となるこのコンポーネントファイルを部品化しておく | |
export default LoginForm; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* ルーティングの構築用ロジック | |
*/ | |
import React from 'react'; | |
//react-native-router-fluxライブラリのインポート宣言 | |
// (ライブラリ概要)このライブラリのGithubリポジトリ | |
// https://github.com/aksonov/react-native-router-flux | |
// 基本的な用法: | |
// Routerタグで各々のSceneを囲む → Sceneにはkeyが設定されており「Actions.key_name()」で遷移する | |
import { Scene, Router, Actions } from 'react-native-router-flux'; | |
//それぞれの表示用のコンポーネントのインポート宣言 | |
import LoginForm from './components/LoginForm'; | |
import EmployeeList from './components/EmployeeList'; | |
import EmployeeCreate from './components/EmployeeCreate'; | |
import EmployeeEdit from './components/EmployeeEdit'; | |
//ルーティングロジック構築用のコンポーネントの内容を定義する | |
const RouterComponent = () => { | |
return ( | |
<Router sceneStyle={{ paddingTop: 65 }}> | |
{ /* 1. Authentication Flow (認証用のフロー) */ } | |
<Scene key="auth"> | |
<Scene key="login" component={LoginForm} title="Please Login" /> | |
</Scene> | |
{ /* 2. Main Flow (メインのフロー) */ } | |
<Scene key="main"> | |
<Scene | |
onRight={() => Actions.employeeCreate()} | |
rightTitle="Add" | |
key="employeeList" | |
component={EmployeeList} | |
title="Employees" | |
initial | |
/> | |
<Scene key="employeeCreate" component={EmployeeCreate} title="Create Employee" /> | |
<Scene key="employeeEdit" component={EmployeeEdit} title="Edit Employee" /> | |
</Scene> | |
</Router> | |
); | |
}; | |
//インポート可能にする宣言 | |
export default RouterComponent; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* アクションのタイプを定義した定数 | |
* → 該当の定数で定義されたアクションを実行することでステートを更新する | |
*/ | |
・・・(省略)・・・ | |
//従業員の一覧・追加・変更に関するアクションの定義 | |
export const EMPLOYEE_UPDATE = 'employee_update'; | |
export const EMPLOYEE_CREATE = 'employee_create'; | |
export const EMPLOYEES_FETCH_SUCCESS = 'employees_fetch_success'; | |
export const EMPLOYEE_SAVE_SUCCESS = 'employee_save_success'; | |
export const EMPLOYEE_DELETE_SUCCESS = 'employee_save_success'; | |
export const EMPLOYEE_REFRESH = 'employee_refresh'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {..., StyleSheet, ...} from 'react-native'; | |
const styles = StyleSheet.create({ | |
container: { | |
flex: 1, | |
justifyContent: 'center', | |
alignItems: 'center', | |
backgroundColor: '#F5FCFF', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment