React Native 快速學習自我挑戰 Day7
Symbols count in article:
2.5k
Reading time:
12 mins.
還沒完成
下一個 App 的概覽
- 這個篇章要講 Navigation,開啟一個新專案
react-native init manager
這個 App 的挑戰
- 需要使用在登入畫面使用 Redux-ify
- Header 的內容需要隨著螢幕去改變
- 每一個使用者都應該要有自己的內容
- 需要能夠打入文字
- 需要全螢幕的 overlay
再稍微做一下設定
- 安裝 react-redux 和 redux 套件,記得 react-redux 是用來串聯 react 和 redux 的套件
npm install --save react-redux redux
- 修改根目錄的 index.js
1 2 3 4
| import { AppRegistry } from 'react-native'; import App from './src/App';
AppRegistry.registerComponent('manager', () => App);
|
- 新增 src/App.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React, { Component } from 'react'; import { View, Text } from 'react-native'; import { Provider } from 'react-redux'; import { createStore } from 'redux';
class App extends Component { render() { return ( <Provider store={createStore()}> <View> <Text> Hello! </Text> </View> </Provider> ); } }
export default App;
|
更多模板設定
- 新增預設的 reducer,新增 src/reducers/index.js
1 2 3 4 5
| import { combineReducers } from 'redux';
export default combineReducers({ banana: () => [] });
|
- 在 src/App.js 引入 reducer 並使用它
1 2 3
| import reducers from './reducers';
<Provider store={createStore(reducers)}>
|
- 安裝 Firebase
npm install --save firebase
- 在 src/App.js 引入 firebase 並載入設定檔案
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import firebase from 'firebase';
componentWillMount() { const config = { apiKey: '', authDomain: '', databaseURL: '', projectId: '', storageBucket:'', messagingSenderId: '' };
firebase.initializeApp(config); }
|
處理資料 React 和 Redux 做比較
在 Redux 世界的登入表單
- 登入表單的四個 component state
- email
- password
- loading
- error
- state 的傳遞方式,「email」、「password」=> 登入確認 => 「loading」、「error」
重建登入表單
- 複製上一個專案的 src/components/common 到 src/components/common
- 新增 src/components/LoginForm.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import React, { Component } from 'react'; import { Card, CardSection, Input, Button } from './common';
class LoginForm extends Component { render() { return ( <Card> <CardSection> <Input label="Email" placeholder="email@gmail.com" /> </CardSection> <CardSection> <Input label="Password" placeholder="password" /> </CardSection> <CardSection> <Button> Login </Button> </CardSection> </Card> ); } }
export default LoginForm;
|
- 在 App.js 引入 LoginForm,然後使用 LoginForm
1 2 3 4 5 6 7 8 9
| import LoginForm from './components/LoginForm';
render() { return ( <Provider store={createStore(reducers)}> <LoginForm/> </Provider> ); }
|
使用 Action Creators 來處理表單更新
- Redux 運作的邏輯
- 使用者輸入內容
- 使用新文字來呼叫 Action Creator
- Action Creator 回傳一個 Action
- Action 傳送給所有 Reducers
- Reducers 計算新的 State
- State 傳送給所有 Componenets
- Components 重新渲染新的畫面
- 等待新的改變…回到第一個動作
- 在 src/components/LoginForm.js 的 Email Input 新增 onChange 事件
1 2 3 4 5
| <Input label="Email" placeholder="email@gmail.com" onChangeText={this.onEmailChange.bind(this)} />
|
- 在 src/components/LoginForm.js 將 onChange 事件獨立
1 2 3
| onEmailChange(text) {
}
|
- 新增 src/actions/index.js 新增 emailChanged 的 Action Creator
1 2 3 4 5 6
| export const emailChanged = (text) => { return { type: 'email_changed', payload: text }; };
|
完成 Action Creator
- 在 src/components/LoginForm.js 跟 Action Creator 做連結
1 2 3 4 5 6 7 8
| import { connect } from 'react-redux'; import { emailChanged } from "../actions";
onEmailChange(text) { this.props.emailChanged(text) }
export default connect(null, { emailChanged })(LoginForm);
|
- 修改 src/reducers/index.js
1 2 3 4 5 6
| import { combineReducers } from 'redux'; import AuthReducer from './AuthReducer';
export default combineReducers({ auth: AuthReducer });
|
- 新增 src/reducers/AuthReducer.js
1 2 3 4 5 6 7 8
| const INITIAL_STATE = { email: '' };
export default (state = INITIAL_STATE, action) => { switch (action.type) { default: return state; } }
|
Typed Actions
- Typed Actions 主要是為了避免寫程式上輸入的錯誤,所以特別把它獨立出來
- 新增 src/actions/types,用 const 的方式輸出
export const EMAIL_CHANGED = 'email_changed';
- 在 src/actions/index.js 引入 types.js,將變數改為 EMAIL_CHANGED
1 2 3 4 5 6 7 8
| import { EMAIL_CHANGED } from "./types";
export const emailChanged = (text) => { return { type: EMAIL_CHANGED, payload: text }; };
|
- 在 src/reducers/AuthReducer.js 的地方也引入 EMAIL_CHANGED
1 2 3 4 5 6 7 8 9 10
| import { EMAIL_CHANGED } from "../actions/types";
export default (state = INITIAL_STATE, action) => { switch (action.type) { case EMAIL_CHANGED:
default: return state; } }
|
不要讓 State 異變
一成不變的 State
- 觀察以下程式碼,最後會發現
newState === state
,原因就是 state 會連結物件,在以下範例中,state 和 newState 連結的會是一樣的物件,最後就會得到 newState === state
的結果
const state = {}
const newState = state
newState.color = 'red';
建立一成不變的 State
- 在 src/reducers/AuthReducer.js 的 case EMAIL_CHANGED 回傳物件 {},這邊的意思是說把所有的 state 讀回來,然後把 email 這個變數放到所有的 state,如果所有的 state 有 email 的話,則會用新的覆蓋
1 2
| case EMAIL_CHANGED: return { ...state, email: action.payload };
|
- mapStateToProps function 用來取得某個部份的 state 到我們的 components 裡面
更多關於建立一成不變的 State
- 在 src/components/LoginForm.js 新增 onPasswordChange 的 function,然後跟資料流連結
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { emailChanged, passwordChanged } from "../actions";
onPasswordChange(text) { this.props.passwordChanged(text); }
// 修改 Password Input 的值 onChangeText={this.onPasswordChange.bind(this)} value={this.props.password}
const mapStateToProps = state => { return { email: state.auth.email, password: state.auth.password }; };
export default connect(mapStateToProps, { emailChanged, passwordChanged })(LoginForm);
|
- 修改 src/actions/index.js 新增 passwordChanged 的 Action
1 2 3 4 5 6 7 8 9 10 11 12
| import { EMAIL_CHANGED, PASSWORD_CHANGED } from "./types";
export const passwordChanged = (text) => { return { type: PASSWORD_CHANGED, payload: text } };
|
- 在 src/actions/types.js 新增 PASSWORD_CHANGED
export const PASSWORD_CHANGED = 'password_changed';
- 在 AuthReducer 新增 PASSWORD 的 case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { EMAIL_CHANGED, PASSWORD_CHANGED } from "../actions/types";
const INITIAL_STATE = { email: '', password: '' };
export default (state = INITIAL_STATE, action) => { switch (action.type) { case EMAIL_CHANGED: return { ...state, email: action.payload }; case PASSWORD_CHANGED: return { ...state, password: action.payload }; default: return state; } }
|
同步和異步的 Action Creators
- 預計使用的 State
- email:當使用者在 email 欄位輸入文字的時候要改變
- password:當使用者在 password 欄位輸入文字的時候要改變
- loading:當開始傳送認證請求的時候為 True,當完成的時候則為 False
- error:預設是 empty string,當我們在認證請求失敗的時候,就要將 error 訊息傳到這個欄位
- user:預設是 null,當成功認證的時候,放到 user model
- 在 js 裡面,執行一個 function 最後都要 return 東西回來,如果有 Ajax 請求的話,就要用特別的方式處理。
介紹 Redux Thunk
- 安裝 redux-thunk
npm install --save redux-thunk
- 預設 Action Creator 規則
- Action Creator 都是 functions
- 一定要 return action
- Action 是一個有 ‘type’ 值的 object
- 使用 Thunk 的 Action Creator 規則 (本來上面的規則一樣可用)
- Action Creator 都是 functions
- 一定要 return function
- function 會使用 dispatch 來呼叫
- Thunk 的方式就可以允許我們用手動的方式去 dispatch 一個 action 來呼叫所有不同的 reducers
- 新增一個 action 到 src/actions/index.js
1 2 3 4 5 6
| import firebase from 'firebase';
export const loginUser = ({ email, password }) => { firebase.auth().signInWithEmailAndPassword(email, password) .then(user => console.log(user)); };
|
運用 Redux Thunk
- 修改 src/App.js 把 redux-thunk 呼叫進來
1 2 3 4 5 6 7 8 9 10 11
| import ReduxThunk from 'redux-thunk';
render() { const store = createStore(reducers, {}, applyMiddleware(ReduxThunk));
return ( <Provider store={store}> <LoginForm/> </Provider> ); }
|
- Redux Thunk 的流程
- Action Creator 被呼叫
- Action Creator 回傳一個 function
- Redux Thunk 看我們回傳了一個 function 並且使用 dispatch 呼叫他
- 我們進行登入的請求
- 等待…
- 等待…
- 請求完成,使用者已登入
- .then 運作
- Dispatch 我們的 action
- 修改 src/actions/index.js 的 loginUser,用 dispatch 的方式回傳
1 2 3 4 5 6 7 8
| export const loginUser = ({ email, password }) => { return (dispatch) => { firebase.auth().signInWithEmailAndPassword(email, password) .then(user => { dispatch({ type: 'LOGIN_USER_SUCCESS', payload: user }); }); }; };
|
- 修改 src/components/LoginForm.js,讓 LoginUser 的 action 可以使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { emailChanged, passwordChanged, loginUser } from "../actions";
onButtonPress() { const { email, password } = this.props;
this.props.loginUser({ email, password }); }
<Button onPress={this.onButtonPress.bind(this)}> Login </Button>
export default connect(mapStateToProps, { emailChanged, passwordChanged, loginUser })(LoginForm);
|
讓 LoginUser 更穩固
- 在 src/actions/types.js 新增 LOGIN_USER_SUCCESS
export const LOGIN_USER_SUCCESS = 'login_user_success';
- 在 src/actions/index.js 新增 LOGIN_USER_SUCCESS,並放到 dispatch 裡面
1 2 3 4 5 6 7 8 9 10 11 12
| import { EMAIL_CHANGED, PASSWORD_CHANGED, LOGIN_USER_SUCCESS } from "./types";
return (dispatch) => { firebase.auth().signInWithEmailAndPassword(email, password) .then(user => { dispatch({ type: LOGIN_USER_SUCCESS, payload: user }); }); };
|
- 在 src/reducers/AuthReducer.js 新增 LOGIN_USER_SUCCESS 並回傳 state
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { EMAIL_CHANGED, PASSWORD_CHANGED, LOGIN_USER_SUCCESS } from "../actions/types";
const INITIAL_STATE = { email: '', password: '', user: null };
case LOGIN_USER_SUCCESS: return { ...state, user: action.payload };
|
建立使用者帳戶
- 在 src/actions/types.js 新增 LOGIN_USER_FAIL
export const LOGIN_USER_FAIL = 'login_user_fail';
- 在 src/actions/index.js 將 LOGIN_USER_FAIL 加入,如果無法登入則創建帳戶,再無法創建帳戶,則彈出錯誤訊息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { EMAIL_CHANGED, PASSWORD_CHANGED, LOGIN_USER_SUCCESS, LOGIN_USER_FAIL } from "./types";
export const loginUser = ({ email, password }) => { return (dispatch) => { firebase.auth().signInWithEmailAndPassword(email, password) .then(user => loginUserSuccess(dispatch, user)) .catch(() => { firebase.auth().createUserWithEmailAndPassword(email, password) .then(user => loginUserSuccess(dispatch, user)) .catch(() => loginUserFail(dispatch)); }); }; };
const loginUserFail = (dispatch) => { dispatch({ type: LOGIN_USER_FAIL }); };
const loginUserSuccess = (dispatch, user) => { dispatch({ type: LOGIN_USER_SUCCESS, payload: user }); };
|
顯示錯誤訊息
- 在 src/reducers/AuthReduer.js 將 LOGIN_USER_FAIL 引入,然後新增 error 的 state
1 2 3 4 5 6 7 8 9 10 11
| import { LOGIN_USER_FAIL } from "../actions/types";
const INITIAL_STATE = { user: null, error: '' };
case LOGIN_USER_FAIL: return { ...state, error: 'Authentication Failed!'};
|
- 在 src/components/LoginForm.js 新增 renderError 的 function,然後把 error 讀出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { View, Text } from 'react-native';
renderError() { if (this.props.error) { return ( <View style={{ background: 'white' }}> <Text style={styles.errorTextStyle}> {this.props.error} </Text> </View> ); } }
{this.renderError()}
const styles = { errorTextStyle: { fontSize: 20, alignSelf: 'center', color: 'red' } };
const mapStateToProps = state => { return { error: state.auth.error }; };
|
Firebase 疑難雜症
- 如果在 src/reducers/AuthReducer.js 的
case LOGIN_USER_SUCCESS:
新增一個 banana;
,實際執行之後就會發現,出現 Authentication Failed 的錯誤訊息,而不是紅色的錯誤畫面,原因在於 firebase 如果執行失敗,就會直接跳到 catch 的語法,導致沒有出現錯誤,而是出現 Authentication Failed
在載入時顯示 Spinner
- 在 src/actions/type.js 加上 LOGIN_USER
export const LOGIN_USER = 'login_user';
- 在 src/actions/index.js dispatch LOGIN_USER
1 2 3 4 5 6 7 8 9 10
| import { LOGIN_USER } from "./types";
return (dispatch) => { dispatch({ type: LOGIN_USER });
firebase.auth().signInWithEmailAndPassword(email, password) ... };
|
- 在 src/reducers/AuthReduer.js,新增 loading 的 case,如果登入成功的話,則把所有值清除,回到預設值
...INITIAL_STATE
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { LOGIN_USER } from "../actions/types";
const INITIAL_STATE = { loading: false };
case LOGIN_USER: return { ...state, loading: true, error: '' }; case LOGIN_USER_SUCCESS: return { ...state, ...INITIAL_STATE, user: action.payload}; case LOGIN_USER_FAIL: return { ...state, error: 'Authentication Failed!', password: '', loading: false };
|
- 在 src/components/LoginForm.js 引入 Spinner,並新增 renderButton 事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { Card, CardSection, Input, Button, Spinner } from './common';
renderButton() { if (this.props.loading) { return <Spinner size="large" /> }
return ( <Button onPress={this.onButtonPress.bind(this)}> Login </Button> ); }
<CardSection> {this.renderButton()} </CardSection>
|