[この記事は David East、デベロッパー アドボケートによる Firebase Blog の記事 "The beginners guide to React Native and Firebase" を元に翻訳・加筆したものです。詳しくは元記事をご覧ください。]

David East
David East
デベロッパー アドボケート

更新情報: Firebase Realtime Database と Firebase Authentication をサポートした Firebase 3.1.0 SDK 向けにこのチュートリアルをアップデートいたしました。

Firebase チームには、多くの React ファンがいます。Firebase はアプリケーションの状態を同期し、React は状態の変化に応じてアプリケーションの UI を再描画します。この組み合わせは完璧です。

さらに、React Native があれば、JavaScript デベロッパーにとってのアプリ開発は、これまで以上に簡単 なものになります。JavaScript だけで本物のネイティブ アプリを作れるようになるからです。なんとすばらしいことでしょう。未来に住んでいるようですね。では早速始めてみましょう。

セットアップ

コードを詳しく見てみたい方は、こちらから最終版の GitHub リポジトリをチェックできます。そうでない方のために、順を追って説明します。

React Native を使ってみるのはとても簡単ですが、いくつか知っておくべきポイントがあります。既に React Native をセットアップしている場合は、このセクションをスキップしてください。

まず、Homebrew が必要になります。これは簡単にインストールできます。さらに、Node.js 4.0 以降も必要です。React Native チームは、ノードのバージョン管理に nvm を使用することを推奨しています (私もお勧めします)

以上のツールがインストールできたら、以下のコマンドを実行します。
brew install watchman
npm install -g react-native-cli

最後に、次の CLI コマンドでプロジェクトを開始します。
react-native init GroceryApp # or whatever you want

お好みのエディタでメインフォルダを開きます。
atom GroceryApp # if you’re into Atom

ビルドと実行

React Native プロジェクトをビルドするには、以下のコマンドを実行します。
react-native run-ios

シミュレータが起動し、ボイラープレート画面が表示されます。
空のアプリ
React Native はホットリロードに対応しています。index.ios.js のコードを編集して Cmd+R を押すと、即座に変更点がアップデートされます。iOS で開発を行っている方をはじめとして、デベロッパーの皆さんはこれがどんなにすばらしいことかおわかりでしょう。

ビルドのセットアップが済んだところで、Firebase を起動して実行してみます。

Firebase のセットアップ

React Native は、npm で依存性を管理しています。Firebase をインストールするには、プロジェクトのルートで次のコマンドを実行します。
npm install firebase --save

index.ios.js を開き、一番上に次の行を追加します。
import * as firebase from 'firebase';

そして、コンポーネントの直前で設定値を渡して Firebase を初期化します。
// Initialize Firebase
const firebaseConfig = {
  apiKey: "<your-api-key>",
  authDomain: "<your-auth-domain>",
  databaseURL: "<your-database-url>",
  storageBucket: "<your-storage-bucket>",,
};
const firebaseApp = firebase.initializeApp(firebaseConfig);

この const とは何でしょうか。これは、読み取り専用の値への参照です。Firebase の値を上書きすることはないので、ここで const を利用するのは筋が通っています。このキーワードを使えるのは、Node.js 4.0 以降を使用しているからです。もしエディタがエラーを表示しても、それはエディタの間違いです。

さらに、もう 1 つの ES2015 機能を使います。React.createClass() を使ってコンポーネントを定義するのではなく、クラスを利用します。

ES2015(ES6)コンポーネント

React はコンポーネントベースで動作します。つまり、アプリはルート コンポーネントから始まる単なるコンポーネントのツリーです。React 0.14 では、ES2015 クラスを使って React コンポーネントを定義できます。

index.ios.js で、React.createClass() ではなくクラスを使用するようにコンポーネントを変更してみましょう。
class GroceryApp extends Component {
  render() {
    return (
      <View style="{styles.container}">

      </View>
    );
  }
}

React.createClass() ではなく ES2015 クラスを使う理由は何でしょうか。このテーマは大きな議論となっていますが、結局これは趣味の問題だという結論になっています。

これで、アプリのシェルは完成です。次は、見栄えをよくし、もう少しきちんとしたアプリにしてみましょう。

React Native によるスタイル設定

React Native では、スタイル設定に CSS ではなく JavaScript を使用します。まったく異なる手法だと感じる方がいらっしゃるかもしれませんが、実際はそれほど異なるものではありません。スタイルを宣言するには、StyleSheet を作成します。
var styles = StyleSheet.create({
  container: {
    backgroundColor: '#f2f2f2',
    flex: 1,
  },
});

StyleSheet には、CSS のような形でスタイルを表現するいくつかのオブジェクトを含めることができます。そのスタイルは React コンポーネントで使用できます。
<View style="{styles.container}">
  Im a container lol!
</View>

そのため、CSS のスキルが無駄になるわけではありません。React Native でスムーズにスタイル設定を行うには、CSS Flexbox を学習するとよいでしょう。

React のスタイル設定もできるようになりましたので、次はアプリのスタイルを宣言します。

スタイルの追加

styles.js という名前のファイルを作成し、このファイルのコードを追加します。記載されているのは、今回のアプリで使用するスタイルです。

React Native は CommonJS モジュールを使っていることに気づく方もいるでしょう。styles.js の最後の部分で、module.exports を使用して StyleSheet をエクスポートしています。

こうすることによって、このスタイルを require() でインポートすることができます。index.ios.js を開き、次の行を追加します。
const styles = require('./styles.js')

ファイルの下部にあるスタイル変数は忘れずに削除してください。

これでスタイルを設定できました。次に、アプリのコンポーネント構造を見てみましょう。

UI をコンポーネント階層に分割する

React について私が最も役立つと感じているアドバイスは、 UI をコンポーネント階層に分割するところから始めることです。次の図は、今回のアプリのコンポーネント階層を視覚的に表現したものです。
アプリの階層
このアプリは、5 つのコンポーネントで構成されています。
  1. GroceryApp(オレンジ): アプリ全体を含みます。
  2. StatusBar(紫): ビューのタイトルを表示します。
  3. ListView(緑): 食料品アイテムのリストを表示します。
  4. ListItem(黒): リストの個々のアイテムを表示します。
  5. ActionButton(青): リストにアイテムを追加します。

components という名前のフォルダを作成します。以上のコンポーネントは、components フォルダに格納します。ただし、GroceryApp は除きます。これは index.ios.js に含めます。

以下の各コンポーネントを components フォルダに追加してください。

ActionButton.js
'use strict';

import React, {Component} from 'react';
import ReactNative from 'react-native';
const styles = require('../styles.js')
const constants = styles.constants;
const { StyleSheet, Text, View, TouchableHighlight} = ReactNative;

class ActionButton extends Component {
  render() {
    return (
      <View style={styles.action}>
        <TouchableHighlight
          underlayColor={constants.actionColor}
          onPress={this.props.onPress}>
          <Text style={styles.actionText}>{this.props.title}</Text>
        </TouchableHighlight>
      </View>
    );
  }
}

module.exports = ActionButton;

ListItem.js
import React, {Component} from 'react';
import ReactNative from 'react-native';
const styles = require('../styles.js')
const { View, TouchableHighlight, Text } = ReactNative;

class ListItem extends Component {
  render() {
    return (
      <TouchableHighlight onPress={this.props.onPress}>
        <View style={styles.li}>
          <Text style={styles.liText}>{this.props.item.title}</Text>
        </View>
      </TouchableHighlight>
    );
  }
}

module.exports = ListItem;

StatusBar.js
'use strict';
import React, {Component} from 'react';
import ReactNative from 'react-native';
const styles = require('../styles.js')
const { StyleSheet, Text, View} = ReactNative;

class StatusBar extends Component {
  render() {
    return (
      <View>
        <View style={styles.statusbar}/>
        <View style={styles.navbar}>
          <Text style={styles.navbarTitle}>{this.props.title}</Text>
        </View>
      </View>
    );
  }
}

module.exports = StatusBar;

コンポーネントを追加したら、まず動きのないアプリを作ってみましょう。

動きのないモックアップ

index.ios.js で、次のインポート文をページの最初に追加します。
import React, {Component} from 'react';
import ReactNative from 'react-native';
import * as firebase from 'firebase';
const StatusBar = require('./components/StatusBar');
const ActionButton = require('./components/ActionButton');
const ListItem = require('./components/ListItem');
const styles = require('./styles.js');

さらに、次のコード スニペットを追加します。
_renderItem(item) {
    return (
      <ListItem item="{item}" onpress="{()" ==""> {}} />
    );
  }

  render() {
    return (
      <View style="{styles.container}">

        <StatusBar title="Grocery List">

        <ListView datasource="{this.state.dataSource}" renderrow="{this._renderItem.bind(this)}" style="{styles.listview}/">

        <ActionButton title="Add" onpress="{()" ==""> {}} />

      </View>
    );
  }

render() 関数でアプリのメインビューを作成し、_renderItem() でリストの個々のアイテムを設定しています。

次に、ルート コンポーネントである GroceryApp のコンストラクタを作成します。
constructor(props) {
  super(props);
  this.state = {
    dataSource: new ListView.DataSource({
      rowHasChanged: (row1, row2) => row1 !== row2,
    })
  };
}

コンポーネントには、state という特殊なプロパティがあります。これは、アプリケーションのデータフロー全体を管理します。この点は、次のセクションで詳しく説明します。

アプリのステートに、ListView.DataSource を設定しています。これは ListView コンポーネントのデータ処理を効率的に行うクラスです。次のステップは、データの表示です。

コンポーネントにはそれぞれのライフサイクルがあり、重要なイベントで所定の関数が呼び出されます。コンポーネントが初めてレンダリングされるときには、componentDidMount() が呼び出されます。この場所で、アプリの初期状態を設定します。
componentDidMount() {
    this.setState({
      dataSource: this.state.dataSource.cloneWithRows([{ title: 'Pizza' }])
    })
  }

アプリをビルドして実行すると、次のような動きのないアプリが表示されます。

各コンポーネントは、単純にデータを表示したり、タップに対してコールバック関数を設定したりしています。

ここで理解しておくべきことは、これらのコンポーネントはステートフルではないという点です。各コンポーネントのプロパティは、ルート コンポーネントである GroceryApp によって設定されています。React を理解するには、ステートを管理する方法を学ぶ必要があります。

ステート

ステートは、ただの変化するデータです。このデータがステートと呼ばれるのは、アプリケーションの「ステート」(状態)を示しているからです。このデータが変化すると、アプリケーションの表示が変化する可能性が高いため、異なる「ステート」になると表現できます。一般的に、ステートは ToDo リストの項目や、有効 / 無効ボタンのようなものです。

ルート コンポーネントはステートのホルダーとなります。ステートの変化はルート コンポーネントから始まります。その際に、ルート コンポーネントは子コンポーネントのプロパティをアップデートします。

コンポーネントのプロパティは不変です。すなわち、更新することはできません。 では、どうやって更新できないものを変更すればよいのでしょうか。 その方法とは、componentDidMount() で見たように、setState() を呼び出してアプリケーションを再描画することです。

setState() は特別な関数です。この関数は、呼ばれるたびにアプリ全体を再描画しようとします。具体的には、子プロパティが前回のステートと異なる場合、新しい値で再描画します。

React と Firebase の相性がとてもよいのはこのためです。Firebase データベースが複数の端末間でアプリケーションのステートを同期し、React が効率的にアプリケーションのステートの変化を再描画します。

リアルタイム データベース リスナー

コンストラクタで、プロパティとして Realtime Database へのリファレンスを作成します。
this.itemsRef = firebaseApp.database().ref();

次に、GroceryApp のコンポーネントに次の関数を追加します。
listenForItems(itemsRef) {
    itemsRef.on('value', (snap) => {

      // get children as an array
      var items = [];
      snap.forEach((child) => {
        items.push({
          title: child.val().title,
          _key: child.key
        });
      });

      this.setState({
        dataSource: this.state.dataSource.cloneWithRows(items)
      });

    });
  }

この関数は、すべての食料品アイテムに対する値のリスナーを作成します。アイテムが追加、変更、削除されると、そのすべての結果が Firebase SDK から DataSnapshot 形式で渡されます。DataSnapshotforEach(child) を使い、すべての子要素に対して反復処理を行って食料品リストのアイテムを示す配列に追加します。.forEach 関数では、DataSnapshot の .key() 値から _key プロパティを生成している点に注意してください。これによって、後ほどデータ操作を行う際の処理がはるかに簡単になります。

配列を設定できたら、ステートの dataSource プロパティを dataSource.cloneWithRows(items) を使ってアップデートします。cloneWithRows() 関数は、以前に定義したものと同じ DataSource に基づいた新しい ListView.DataSource を作成する便利なメソッドです。

次に、componentDidMount() にアイテムをリッスンするコードを記述します。
componentDidMount() {
    this.listenForItems(this.itemsRef);
  }

アプリをビルドして実行すると、空のページが表示されるはずです。しかし、Firebase App Dashboard またはすばらしくクールなデータビューア Vulcan からいくつかのアイテムを追加すると、表示がリアルタイムにアップデートされることがわかります。

Vulcan

これはすばらしいことですが、「Add」ボタンも動作させる必要があります。この点は、次のセクションで対応します。

アイテムの追加

ActionButton をタップした際に、アラートをポップアップさせてユーザーがアイテムを入力できるようにします。AlertIOS API を使うと、このアラート ボックスを生成できます。

アイテムの追加
GroceryApp のコンポーネントに次の関数を追加します。
_addItem() {
    AlertIOS.prompt(
      'Add New Item',
      null,
      [
        {
          text: 'Add',
          onPress: (text) => {
            this.itemsRef.push({ title: text })
          }
        },
      ],
      'plain-text'
    );
  }

AlertIOS API は、自由に拡張したアラートを構築することができます。最初の 2 つのパラメータはシンプルで、アラート ボックスのタイトルとメッセージ(省略可能)です。この API で重要になるのは、3 つ目のパラメータです。ここで、ユーザーが利用できるボタンを指定する配列を作成します。各ボタンには、textstyleonPress コールバック関数を指定できます。最後のパラメータは入力タイプで、plain-text または secure-text を指定します。

アイテムを追加するには、ボタン配列の中にオブジェクトを作成します。このオブジェクトで、onPress コールバックを使ってアイテムを追加できます。このコールバックは、ユーザーが入力したテキストを受け取ります。このテキストを .push() に渡し、新しい子要素を /items ロケーションに作成します。

次に、render() を改訂して ActionButton の onPress プロパティを割り当てます。
<ActionButton title="Add" onpress="{this._addItem.bind(this)}">
</ActionButton>

ビルドして実行し、Add ボタンをタップしてアイテムの名前を入力すると、リストがアップデートされるはずです。

すばらしいですね。しかし、完了したアイテムを削除できない食料品リストはあまり役に立ちません。

アイテムの完了

では、ユーザーがアイテムを完了できるようにしましょう。アイテムをタップするとアラート ボックスが開き、そこで「Complete」を押すと、リストから削除されるようにします。

_renderItem(item) を変更して onPress コールバックを追加します。
_renderItem(item) {

    const onPress = () => {
      AlertIOS.prompt(
        'Complete',
        null,
        [
          {text: 'Complete', onPress: (text) => this.itemsRef.child(item._key).remove()},
          {text: 'Cancel', onPress: (text) => console.log('Cancel')}
        ],
        'default'
      );
    };

    return (
      <ListItem item="{item}" onpress="{onPress}">
    );
  }

アイテムを「完了」するためには、Firebase データベースからアイテムを削除しなければなりません。.child(key) を使うと、リストの特定の項目を取得することができます。onPress コールバックはクロージャなので、item パラメータを含む外側のコードにアクセスできます。ここで、先ほどの _key プロパティが役立ちます。

「Complete」がタップされた際に、item_key プロパティを使って特定の子要素を検索することができます。そして、.remove() を呼び出して Firebase データベースからアイテムを削除します。

再度ビルドして実行し、いずれかの ListItem をタップして「Complete」を選ぶと、リストからアイテムが削除されるはずです。

コードを取得してスターマークを

完全なアプリは Github からチェックアウトできます。よろしければ、ぜひスターマークをお付けください。リポジトリは自由にフォークしてください。PR をお送りいただいても構いません。

お困りの場合

何か問題がありましたら、Stackoverflow で質問してください。私たちは Firebase タグを頻繁に確認しています。もしくは、コミュニティ Slack チームにご連絡ください。


Posted by Eiji Kitamura - Developer Relations Team