はじめに
個人的に元々のキャリアがWebに近しいところからのスタートであったので、JavaScriptに関する部分もiOSやサーバーサイド同様にキャッチアップしていきたいという思いがあったことや、先日参加させて頂いたTry!Swiftの中でもReactNativeに関連するお話をお聞きする機会があったことも踏まえて、今年から取り組んでみていました。
今回は自分のこれまでReactNativeを始めるにあたって、「こんな感じで取り組んでみました。」という備忘録と調べながらのサンプル作成において活用したものや参考リンク等をざっくりとまとめてみました。
あまりコードそのものに関する解説はない記事にはなってしまうかと思いますが、取り組む上で参考にした記事のリンクや自分で作成したリポジトリ内のコードや動きに関する部分はできるだけ掲載していきますので、これからReactNativeに携わる際の少しでも参考になれば幸いに思います。
- 2017.04.02: 追記
記事の内容に関して、ポイントになりそうな部分に関するコードについての記述を追加しました。
1. ReactNativeをはじめてみるための準備と基本的な部分について触れてみる
ReactNativeを始めるにあたり、なかなかドキュメントを読んだりするだけでは掴みきれないのではないかと思ったので、まずは簡単なところからで良いので「Step By Step」で少しずつ取り組んで行こうというような方針で進めていくような形にしてみました。
★1-1. 環境構築やセットアップに関する部分のこと
ReactNativeの環境構築等に関しては、下記のような手順で作成することができます。
自分の環境の場合では、元々node.jsをbrew経由でインストールしていなかったことやnode.jsのバージョンが古かったこともあり、下記のような形でインストールとプロジェクトの作成をしました。
//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
導入方法やその他基本的なコンポーネント等の事項に関しては、下記のリンクにReactNativeに関する公式ドキュメントがあるので一読をして置くと良いかと思います。
★1-2. ReactNativeのはじめの一歩として作成したサンプルに関して
ドキュメントをわからない状態のままで構わないので、一通り読み進めた後には下記の動画での解説とサンプルを写経して読み進めていくような形で進めていきました。
参考にした解説動画:
上記のリンクで掲載した解説動画に関しては、React及びReactNativeの基本的な書き方の部分やFlexboxでのレイアウト作成の基本的な部分に関する解説に加えて、実際に簡単なストップウォッチのアプリを作成するチュートリアルが紹介されています。下記は動画で紹介されていたサンプルソースに関して写経をした後に、自分なりに日本語でのドキュメンテーションを行ったものになります。
作成したリポジトリ:
また、いきなり自分でサンプルを作成する形ではなく、YouTube内で紹介されているサンプルの写経をやっていきました。必要と感じた部分やJavaScriptの書き方等に関する補足事項に関しては、READMEやソースコード内に参考になった記事へのリンクに関してもまとめてありますので、参考になれば幸いです。
下記のソースコードは解説動画の中で、簡単なストップウォッチアプリの作成サンプルに取り組んだ際のコードになります。
このサンプルの中では、
- ReactNativeを始めるにあたってのコンポーネントの書き方
- Flexboxによる簡単なレイアウト作成
- ステートによる状態変化の保持と活用
- 簡単なライブラリの扱い方
に関して学ぶことができました。若干JavaScriptの記載方法については甘い部分がある感じではありますが、コンポーネントやステートの扱いのイントロダクションとして大まかに肌感を掴むのには良いかもと思いました。
/**
* 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);
2. ReactNativeに取り組んだ際に最初に押さえておいた部分のまとめ
ReactNativeに取り組む前にはまずは、Reactに関する基本的な理解(コンポーネントやVirtualDOM等)をしておく必要があります。
Reactに関する基本的な理解や概念の整理をする際には、後述のJavaScriptやReactに関する書籍と併せてReactに関する基本的な考え方の整理に下記の記事も参考にしました。
そして実際にReactNativeのサンプルソースの読解を進めていく際にReactの基本的な事項と併せて、さらに必要な下記の要素についてはしっかりと整理していきたい部分については下記のような形にまとめています。
★2-1. StateとPropsに関する簡単なまとめと参考資料
それぞれコンポーネントの状態管理を行う上で、まずはしっかりと押さえておかなければいけないのがProps
とState
になります。ざっくりと役割を言葉でまとめると下記のようになります。
- State(ステート): コンポーネントの内部変数のようなものでコンポーネントの状態変化を保持するために使用する。またコンポーネントの外部からはアクセスできない。
- Props(プロパティ): コンポーネントの外部から与えられるオプションのようなもので親のコンポーネントから子のコンポーネントに渡される値。またコンポーネントはPropsの値を変更してはいけない。
PropsとStateに関する簡単なまとめ:
Stateの更新について:
また、PropsとStateの役割や概念に関する理解を深める上では下記の記事や資料を参考にしました。
- ReactにおけるStateとPropsの違い
- React超入門。propsとstateの動きを理解するコード
- 【Reactのstateとpropsの違いが知りたい!(変更・更新の仕方等デモあり)】過去のReact初心者の自分にpropsとstateの違いを説明する
- 今からはじめるReact.js〜propsとstate、それからrefs〜
特に後述するReduxを理解する際には、PropsとStateの概念や活用方法に関する知識がかなり重要になってくる部分になります。
★2-2. Flexboxでのレイアウトの作り方や書き方に関する参考資料
ReactNativeの画面のレイアウトに関しては、下記のような記載方法で各々のコンポーネントに対してスタイルを作成していく形になります。
肌感としてはCSSでのスタイルの定義とかなり似た感じで進めていくことができますが、スネークケースではなくキャメルケースで記載する点が従来のスタイルシートの記載と異なる点でしょうか。
import {..., StyleSheet, ...} from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
});
またFlexboxの概念やレイアウト作成に関するものについては最初はなかなか慣れなかったこともあり、下記の記事や資料を参考にしました。
- React Nativeのデザイン - Stylesheet/Flexbox (part1)
- React Nativeのデザイン - Stylesheet/Flexbox (part2)
- A Complete Guide to Flexbox
- ReactNativeのFlexboxレイアウト
- React Native Styling Cheat Sheet
この辺りのUIレイアウトの部分に関しては個人的にかなり関心の高い部分ではあるので今後もしっかりと追っかけて行ければと思います。
★2-3. ReactComponentのライフサイクル
こちらに関しても下記にピックアップした参考記事を元に、自分なりにノートに書いてまとめてみました。
ネイティブのiOSアプリ開発においてもライフサイクルの理解が重要になってくるように、ReactNativeのアプリ開発においても同様に重要な部分になってきます。
- React.Component(ReactNative公式ドキュメント)
- React component ライフサイクル図
- React Componentのライフサイクルのまとめと利用用途
- React コンポーネントのライフサイクルについて
★2-4. Reduxのフローチャートとそれぞれの役割
Reduxはアプリ開発の中で肥大しやすくかつ複雑になりやすいstate(状態)
の変化に関する管理をするためのものになります。データの流れを一方向(Actions -> Reducers -> Storeという流れ)にして、アプリ内のstate(状態)
をStore内にて一括で管理することによって、現在のアプリの状態遷移をわかりやすくする役割を果たしてくれます。
今回のサンプル内ではデータの追加・変更・削除といったデータが絡んでくる処理やログイン・ログアウトの認証状態の管理に活用しています。
下記に記したノートに関しては、下記の内容を参考に「キモになりそうな部分」をイメージにして自分でメモしたものです。
この部分については他の資料も参考にした上で実際に題材となるサンプルと一緒に取り組むとより効果的に理解が深まるのではと思います。
Reduxの簡単なフロー図まとめ:
1. それぞれの役割の説明:
- Action:「何をする」という情報を持ったオブジェクト
- ActionCreateors:Actionを生成するメソッド ⇨
dispatch
をしてcreator(ActionCreateors)
で作成したaction
を渡す - Reducers:
action
を受けてstate
を変更する ⇨action
とstate
を受け取って新しいstate
を返す - Store:ReducersとActionCreateorsのハンドリング ⇨ アプリケーションの状態の保持(
getState()
:stateへのアクセス、dispatch(action)
:state更新、subscribe(listener)
:リスナー登録のメソッドを提供)
2. Reduxアプリケーションのデータライフサイクル:
-
store.dispatch(action)
を呼ぶ -
store
は受けたaction
と現在保持しているstate
をreducer
へ渡す - 子の
reducer
の返したstate
を親のreducer
がまとめて1つの大きなツリー状のstate
を返す -
reducer
が作成した新しいstate
をstore
が保持する
また一番最初にReactNative + Reduxの組み合わせのサンプルを試したりまとめたりする前には、下記の記事も目を通しました。
Reduxに関する基本事項の整理と理解をする際には、サンプルに取り組む際にはコードの中にできるだけコメントと理解の参考にした記事のリンクなども一緒に掲載し、基本概念やフローの図解等に関してはノート等に図解として残しておくとより自分の中で整理できると同時に理解も早まる気がしたので、自分でアプリを作成する際にもできるだけ設計の足跡を残しておくようにしています。
(後述のUdemyのオンラインコースを利用して作成したeduxを使ったサンプルでもReduxのそれぞれの処理に関するポイントをコメント内に残しています)
この記事ではReactNativeに触れる際に最低限理解するのに参考になった基本的な事項に関してざっくりとまとめた形なので、
3. Udemyのオンラインコースを利用してReduxやデータを伴う挙動に関して触れてみる
簡単な基本事項の整理が終わったあとは、ReactNativeを活用してもう一歩踏み込んだサンプルを作成するプラクティスをこなしてみようと感じたので、Udemyで下記の講座を受講してみました。
このコースではReactNativeでの基本的なコンポーネントの作成方法から、非同期処理のサンプルをはじめ、実際にfirebaseを活用した認証画面を作成するサンプルやReduxを活用したサンプル等が取り揃えられており、ドキュメントを見るだけではイマイチ掴みにくかったReduxの概念とコードとの関係性が理解できるようになったのは、個人的には良い収穫であったなと感じました。
1. コンポーネントの活用とAPIからのデータ取得と表示のサンプル:
このサンプルに関してポイントとなる部分は、コンポーネントを使い回す方法や通信処理を利用する際の実装部分がポイントになるかと思います。このサンプルではAxios
というライブラリを使って非同期通信部分のロジックを作成していますが、fetch
メソッドを使用しての処理で記載してもここは良いかもしれません。
このサンプルでは、アルバム表示データ1セットに対して<AlbumDetail key={album.title} album={album} />
コンポーネントに切り出した形で表示するようにしています。
※この中に表示されているパーツに関しては、更に別の共通コンポーネントで切り出して管理をするようにしています。
/**
* アルバム一覧用のコンポーネント
*/
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;
非同期処理のハンドリング部分に関しては、実際にアプリを作成する機会では活用する部分になってくるので、ライブラリの有無に関わらず処理の書き方をやPromiseに関する知識に関しては下記の記事が参考になりました。
2. Firebaseの認証機能を利用したサンプル:
このサンプルに関してポイントとなる部分は、Firebaseの認証機能を利用してログイン・ログアウトの状態をステートで保持して管理する部分がポイントになるかと思います。
//コンポーネントの内容がMountされる前に行う処理
componentWillMount() {
//firebaseのセッティング情報を記載する
//※ API情報に関してFirebaseコンソールを取得 → Authentication → 「ログイン方法」でメール/パスワードを有効にする
firebase.initializeApp({
apiKey: "XXX",
authDomain: "XXX",
databaseURL: "XXX",
storageBucket: "XXX",
messagingSenderId: "XXX"
});
//認証状態が変更された際の処理
//※取得できたuserオブジェクトを取得してログイン状態のステートを更新する
firebase.auth().onAuthStateChanged((user) => {
if (user) {
this.setState({ loggedIn: true });
} else {
this.setState({ loggedIn: false });
}
});
}
このサンプルにおいてはFirebaseのログイン・ログアウトの状態をsrc/app.js
においてcomponentWillMount()
のタイミングで取得し、その状態をによって画面表示を分ける形になっています。
また、ログイン状態に関する部分に関してはLoginForm.js
という画面単位で分割を行うようにして、更に共通で使い回しができるように__「ボタン・タイトル・ヘッダー等」__という部分に関しては、それぞれ小さなコンポーネントとして切り出してあります。
/**
* ログイン用のフォーム部分のコンポーネント
*/
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="user@gmail.com"
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;
3. Reduxを活用したサンプル:
上記2つのサンプルの様にシンプルな形のものであれば、それぞれのコンポーネントで定義したステートの状態だけで管理しても特に不都合はありませんが、データのCRUDを伴ったりする場合等をはじめ、複雑な状態管理を管理しやすくするめにReduxを用いてステートの流れを1つの場所に管理して、必要な部分だけ値を更新したりしようとするために使うものです。
特にこのサンプルの中に掲載されている__従業員の管理アプリサンプル__の部分の実装に関しては、必要なライブラリを含めてReduxを用いての状態管理をする処理のメリットを掴むのに非常に役に立った様に思います。
下記に画面遷移と従業員データの追加・変更・削除に関連するロジックのファイルを掲載します。
★その1:画面遷移に関する部分について
srcフォルダの直下にはjs:src/app.js
があり、ここではReduxに関する設定(reducerからstoreに保持しておくものの定義とMiddlewareの設定)やfirebaseの認証に関するもの等の基本的な設定を行なっています。
/**
* アプリケーション構築用のファイル
*
* (参考) 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;
srcフォルダの直下には同様にjs:src/router.js
があり、これでそれぞれの遷移画面の管理を行っています。
※遷移の管理には、react-native-router-fluxを活用しています。
このサンプルではfirebaseによる認証があるので、ログイン状態でない場合には<Scene key="auth">
内に定義をしたコンポーネントを表示して、認証を通過した際に<Scene key="main">
内の従業員の追加・変更・削除画面に遷移するようにしています。
/**
* ルーティングの構築用ロジック
*/
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;
画面遷移の構築の仕方やステート値をStore内に一元管理して、その状態変化に応じて処理を行う部分はネイティブアプリの考え方とは大きく異なる部分だったので最初のうちはなかなか理解するのに苦戦しました。
★その2:従業員の一覧表示とデータの追加・変更・削除に関する部分について
firebaseに登録した「従業員データの一覧表示」とfirebaseへの「データの追加・変更・削除」をする部分のAction/Reducer/Component部分のコードを抜粋したものになります。
◉Action:
/**
* アクションのタイプを定義した定数
* → 該当の定数で定義されたアクションを実行することでステートを更新する
*/
・・・(省略)・・・
//従業員の一覧・追加・変更に関するアクションの定義
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';
/**
* 入力された従業員データを格納するステートを更新するためのアクション群
* → 各々のアクションが実行されると対応する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' });
});
};
};
◉Reducer:
Reducerに関しては下記のように役割ごとに分けています。
-
EmployeeReducer.js
→ firebaseから現在登録されている従業員データをセットしてステートに渡す -
EmployeeFormReducer.js
→ 現在の入力フォームの状態データをセットしてステートに渡す
/**
* 従業員一覧表示時の状態ステータスを管理するための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;
}
};
/**
* 従業員データの追加・変更時の状態ステータスを管理するための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;
}
};
◉Components:
それぞれのコンポーネントに関しては、connect()
を用いてReduxのステートの状態と各々のコンポーネントのプロパティをマッピングすることでデータのやり取りをするようなイメージになるかと思います。
/**
* 顧客一覧表示用部分のコンポーネント
*/
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);
こちらはデータの新規追加及び該当データの更新用フォームに関する部分になります。新規追加と変更に関してははじめから表示されているデータが異なるだけなので、この部分は共通化を図るようにし、employeeUpdate
メソッドはフォームの入力状態をステートで管理するためのメソッドになります。
/**
* 顧客情報入力フォーム用部分のコンポーネント
*/
//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);
firebaseを利用して従業員データの新規追加と変更の画面に関しては、
- EmployeeCreate.js → データの新規追加
- EmployeeEdit.js → データの変更及び削除
という形にしています。
/**
* 顧客情報新規追加用部分のコンポーネント
*/
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);
/**
* 顧客情報編集用部分のコンポーネント
*/
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);
この部分についてはできるだけ細かく、「従業員の管理アプリサンプル」のリンクに掲載しているソースにコメントを残したのでご参考になれば幸いに思います。また他のサンプル実装コードに関しても、全てを掲載するとかなり長くなりそうな感じだったので、今回はおおざっぱではありますが、処理のキモとなりそうな部分だけをピックアップして掲載する形にしました。
4. 自分なりに気をつけたことやまとめ方のポイントまとめ
今回取り組んだサンプルが全て英語であったことや、始めた時点では「何となく知っている」ような不慣れな状態であったので、下記の3つのことを意識しながら取り組んでみました。
- 1. ただ書かれた通りに実装するのではなく必要な部分はノート等にまとめておく
- 2. わからない部分に出会った場合にはコメントと参考リンクを残す
- 3. コメントはなるべく自分の言葉にする
この取り組み方だと時間も結構かかる上に割と冗長な感じのスタイルにはなりますが、こうすることで次に時間が経過した際や自分でアプリを作成した際に復習がしやすくするためのある程度目星をつける意味でも有益かなと思います(あくまでもここは私個人の取り組み方なので恐縮ではありますが...)。
5. この記事をまとめる&コードを写経するプラクティスを進めるにあたり取り組んでいる書籍
サンプルを作成するプラクティスを通じていく中で「自分のJavaScriptの書き方がES6にキャッチアップしていないかも...><」と強く感じた部分もあったり、Reactの基本的な部分やReactNativeのソースコードを触る上では書籍も併せて進めていました。
まだまだ私自身もReactNativeに関しては多くの事ができるわけでは決してありませんが、現在自分が個人的に読み進めている書籍に関しても下記にご紹介しておきます。
★5-1. JavaScript及びReact.jsに関する基本事項の整理のために活用
JavaScriptの基本部分に関する理解:
React.jsの概念に関する理解:
こちらはReactNativeと直接的な関連性はないかもしれませんが、元々WebでReactを触っていた経緯があったのでReactに関する基本的な概念の復習や整理をするためにこの書籍も併せて読みました。
★5-2. ReactNativeに関する基本事項の整理のために活用
こちらで紹介する書籍は全て洋書にはなってしまいますが、Githubでソースコードも一緒にあるので英語に抵抗があったとしてもソースコードと併せて動きを見たり読み進めていく事で理解がより深まるのではないかと感じました。特にReduxに関する理解やReduxを絡めたアプリ開発の手順や理屈に関しては、掴むのになかなか苦戦した部分でもあったので、実際にコードを書いて「ああ、こんな感じでデータのやり取りをしていくのか」という肌感を掴んだり、UIを実装する上での参考にはとても役に立つ感じがしました。
Mastering React Native:
React Native Cookbook:
特に上記の2つの洋書に関しては、きっちりと理解して実際のアプリ開発やサンプル作成の課程の中でガンガン活用していこうと感じています。またReduxを利用したstateのやりとりに関しては、下記のリポジトリのように自分で写経と併せてActionとReducerに関するテストもささやかではありますが(このテストが果たして必要なのかはわかりませんが...)書いたりもして色々試したりしている感じです。
※ 上記のソースは「Mastering React Native」での掲載サンプルに自分なりのドキュメンテーションを加えたものになります。
あとがき
ネイティブのiOS開発とは勝手が違う部分(特にレイアウトを構築する部分)に関しては若干最初はちょっと戸惑いそうにはなる部分はあれども、iOSとAndroidで同じコードを使用できる点や、見た目部分をコンポーネントでそれぞれ管理することもできるので、なかなか興味深い点が多いなという印象を受けました(レイアウトの組み方についてはAndroidに近い印象)。
ただ、Reduxの理解に関する部分やデータの連携に関する部分については、実際にアプリ開発のプロセスの中で理解をしないとなかなかピンと来ない部分もありました。
このような場合については、Firebase等を活用したデータのCRUD処理を伴うようなアプリの中でReduxを導入してみて、サンプル試してみる形で進めていくと、うまく概念を理解することができたように思います。
また個人的にReactNativeのUI実装に関するライブラリを調べる機会があったので、下記のUI系のライブラリに関しても色々と調査をしていました。
色々と調べてみるとネイティブアプリでの実装する際のデザイン的な参考にできそうだなと感じる点もあったので、まずは実際にReactNativeでアプリを作成する課程の中で色々と試してみていきたいと思いました。ネイティブアプリやサーバーサイドに加えて、ReactNativeについてもしっかりとモノにしていきたいと感じた次第です。(次こそはTipsしっかり掲載できるようにしなければ...)
また、まだまだ始めたばかりなのでサンプルソースの誤りやご指摘事項等がありましたらよろしくお願い致しますm(_ _)m