React Nativeハンズオン〜initからtodoアプリ実装まで〜

React Nativeのハンズオン用に `react-native init <プロジェクト名>` してからTodoアプリを作るまでの流れを記事にします。store管理にMobXを使う予定ですが、他に使いたいものがある場合は適宜読み替えてください。また、限られた時間で、説明しづらいところはぼかしています。詳しく知りたい方はドキュメントなどを参考にしてください。

ハンズオンでできる予定のコードは下記に公開しています。
https://github.com/OshiroSeiya/AwesomeProject

# 対象者

- ワタシハジャバスクリプトチョットデキル。って方
- React Nativeのざっくりとした使い方知りたいって方
- Mac使ってる人(ios版で書くのでandroidがやりたい人は読み替えてください)

# 必要となる周辺知識

最低限カッコ書きの部分のイメージがついてるとわかりやすいと思います

- Babel(いい感じに変換してくれる)
- React(いい感じに描画してくれる)
- MobX(state管理をReactから奪う)

# 環境構築

node, yarn, watchman, react-native-cliのインストール
(yarnは好みなので入れてますがnpmでも問題ないです)

```sh
$ brew install node
$ brew install yarn
$ brew install watchman
$ npm install -g react-native-cli
```

上記のツールがインストールできているか確認しましょう

```sh
$ node -v
v8.2.1
$ yarn --version
0.27.5
$ watchman -v
4.7.0
$ react-native -v
react-native-cli: 2.0.1
react-native: n/a - not inside a React Native project directory
```

[Xcodeインストールページ](https://itunes.apple.com/jp/app/xcode/id497799835?mt=12)

Xcodeはインストール後に一度起動して利用規約的なものを許可する必要があります。
※僕の手元でXcodeのインストールに30分ぐらいかかりました。

# プロジェクト作成

今回は公式のサンプルに合わせて、AwesomeProjectって名前で作ることにします。

```sh
$ react-native init AwesomeProject
```

ディレクトリ移動して実行できるか確認します。
うまく行けば、iOSシミュレータが起動します。

```sh
$ cd AwesomeProject
$ react-native run-ios
```

初回ビルドなのでそこそこ時間かかります。

`Welcome to React Native!` がシミュレータに表示されていればokです

## うまくいかない場合は

- 環境構築時にすべてインストールできているかもう一度確認しましょう
- Xcodeの利用規約的なものを許可されてるか起動して確認しましょう(たまにアップデート来てて昔okしたのにまた聞かれてることがあります)

# ハンズオン開始

## ディレクトリ構造の変更

初期のディレクトリ構造は下記です。

```sh
$ tree -L 1
.
├── __tests__        // テストコード入れる場所(今回は使わない)
├── android          // Androidのプロジェクト(基本的ネイティブの機能を使うとき以外触らない)
├── app.json         // プロジェクト名など
├── index.android.js // Androidのエントリーファイル
├── index.ios.js     // Iosのエントリーファイル
├── ios              // Iosのプロジェクト(基本ネイティブの機能を使うとき以外触らない)
├── node_modules     // パッケージたち(インストールしたら入る)
├── package.json     // パッケージ情報記載
└── yarn.lock        // パッケージのバージョン情報記載(自動生成)
```

androidとiosで共通のコードをまとめたいので `src` ディレクトリを生成して `main.js` に共通処理をまとめて、エントリーファイルから呼び出せるように変更します。

```sh
$ tree -L 1
.
├── __tests__
├── android
├── app.json
├── index.android.js
├── index.ios.js
├── ios
├── node_modules
├── package.json
├── src               // これを生成した！
└── yarn.lock
```

[main.js](https://github.com/OshiroSeiya/AwesomeProject/blob/b5ad7a686c1944b9f13da8cf2918bc0d89e21ba5/src/main.js
)と[index.ios.js](https://github.com/OshiroSeiya/AwesomeProject/blob/b5ad7a686c1944b9f13da8cf2918bc0d89e21ba5/index.ios.js)をコピーして配置してください。

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/b5ad7a686c1944b9f13da8cf2918bc0d89e21ba5

`Welcome to React Native!` という文字列を変更してシミュレーターに反映されればokです。


## パッケージの追加

MobXが使いたいやら、かんたんに見た目よくしたいやら、オブジェクトのスプレッド演算子が使いたいやら、デコレーターが使いたいやらあるので下記のパッケージを追加します。

```sh
$ yarn add babel-preset-react-native-stage-0 babel-plugin-transform-decorators-legacy -D
$ yarn add mobx mobx-react react-native-vector-icons react-native-elements
```

デコレーター使いたい、スプレッド演算子使いたい、その他諸々の理由で下記に変更します。

```diff:.babelrc
{
-  "presets": ["react-native"]
+  "presets": [
+    "react-native",
+    "react-native-stage-0"
+  ],
+  "plugins": [
+    "transform-decorators-legacy"
+  ]
}
```

https://github.com/OshiroSeiya/AwesomeProject/commit/b97b3e2fd36d657362db930c2792b41e314a0a54

ネイティブコードが含まれているパッケージはlinkを貼る必要があります。
例えば、 `react-native-vector-icons` が該当します。

```sh
$ react-native link
```

自動生成なので見る必要ないけど一応サンプルは下記で見れます。

https://github.com/OshiroSeiya/AwesomeProject/commit/c1930aece1d71533ac7b028776d3e208038a3cc4

## mock作成する

下記のようなmockを作成します。

![mock_1.png](https://qiita-image-store.s3.amazonaws.com/0/82745/d287541c-3ae0-b571-1cc9-33915c9c7e46.png)


### データ構造

todoの構造を下記とします。

```js:src/components/index.js
const todoList = [
  { id: 1, message: 'todo!', isComplete: false, },
  { id: 2, message: 'complete todo!', isComplete: true, },
  { id: 3, message: 'complete todo!', isComplete: true, },
];
```

### React　Native　Elementsのパッケージを使ってみる

`react-native-elements` の `CheckBox` Componentを使って実装します。[参考](https://react-native-training.github.io/react-native-elements/API/checkbox/)

配列をループさせてDOMを生成する場合は `key` を入れる必要があります。

`title` はチェックボックスに表示する文字列が入ります。
`checked` はチェックをつけるかどうかを判断します。
`on~` はイベント処理を定義します。

```js:src/components/index.js
const TodoCheckBox = (row) => (
  <CheckBox
    key={row.id}
    title={row.message}
    onPress={() => console.log('todoトグル')}
    onLongPress={() => {
      Alert.alert(
        '確認',
        `[${row.message}]を削除しますか？`,
        [
          { text: '削除しない', style: 'cancel' },
          { text: '削除', onPress: () => console.log('todo削除') },
        ],
      );
    }}
    checked={row.isComplete}
  />
);
```

### Mock全体

`View` はwebで言う `div` 的なものだと思ってください。
`View` ではスクロールできないので `ScrollView` も使用します。

cssで画面のレイアウト設定を行います。[参考](http://facebook.github.io/react-native/releases/0.47/docs/stylesheet.html#stylesheet)
`flex` は超絶難しいので、詳しく知りたい人は調べてください。

`react-native-elements` の `FormInput` を利用しています。詳しく知りたい人は[参照](https://react-native-training.github.io/react-native-elements/API/forms/)

```js:src/components/index.js
export default class Todo extends Component {
  render() {
    const TodoCheckBox = (row) => (
      <CheckBox
        key={row.id}
        title={row.message}
        onPress={() => console.log('todoトグル')}
        onLongPress={() => {
          Alert.alert(
            '確認',
            `[${row.message}]を削除しますか？`,
            [
              { text: '削除しない', style: 'cancel' },
              { text: '削除', onPress: () => console.log('todo削除') },
            ],
          );
        }}
        checked={row.isComplete}
      />
    );

    return (
      <View style={{ flex: 1 }}>
        <View style={{ marginTop: 20, flexDirection: 'row'}}>
          <View style={{ flex: 5 }}>
            <FormInput
              autoCapitalize="none"
            />
          </View>
          <View style={{ flex: 1 }}>
            <Button
              onPress={() => console.log('add')}
              title="ADD"
            />
          </View>
        </View>
        <ScrollView style={{ flex: 1 }}>
          {todoList.map(TodoCheckBox)}
        </ScrollView>
      </View>
    );
  }
}
```

下記を実行して見た目を確認してください。

```sh
$ react-native run-ios
```

サンプルは下記でみれます。

https://github.com/OshiroSeiya/AwesomeProject/commit/6e8dd54e5eb70827ce4c6204ef5fa72c32cd6359

## ストアー作成する

ストアーとはデータを保持するクラスです。
計算処理やデータ取得処理、イベント処理などを書いたりもします。

`@observable list = []` を利用して画面描画した際、変数変更時に自動的に画面に反映されます。

[こちらを参照](https://mobx.js.org/refguide/observable.html)


```js:src/stores/todo.js
import { observable } from 'mobx';

export default class TodoStore {
  @observable list = [
    { id: 1, message: 'todo!', isComplete: false, },
    { id: 2, message: 'complete todo!', isComplete: true, },
    { id: 3, message: 'complete todo!', isComplete: true, },
  ];
}
```

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/4f4e7d4d79bf174d2c2b0d61ce8a23bac88e8871

`Provider` でComponentを囲むと中でストアーを使うことができます。
[詳細はこちら](https://facebook.github.io/react/docs/context.html)

```diff:src/main.js
import React from 'react';
+import { Provider } from 'mobx-react/native';
import Todo from './components';
+import TodoStore from './stores/todo';
+
+const stores = {
+  todo: new TodoStore(),
+};

export default () => (
-  <Todo />
+  <Provider {...stores}>
+    <Todo />
+  </Provider>
);
```

Componentに先程作った `todo` storeを渡していきます。

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/13f9577564c056e40340360d97debab0d4bfe7a4

まず、モック用のデータ消去します。

```diff:src/components/index.js
- const todoList = [
-   { id: 1, message: 'todo!', isComplete: false, },
-   { id: 2, message: 'complete todo!', isComplete: true, },
-   { id: 3, message: 'complete todo!', isComplete: true, },
- ];
```

ストアーを使うためには, `inject` と `observer` を使用します。
おまじないとして覚えてください。
[こちらを参照](https://mobx.js.org/refguide/api.html)

```diff:src/components/index.js
+ import { inject, observer } from 'mobx-react/native';

+ @inject('todo')
+ @observer
export default class Todo extends Component {
  render() {
+   const { todo } = this.props;
    return (
```

```diff
         <ScrollView style={{ flex: 1 }}>
-          {todoList.map(TodoCheckBox)}
+          {todo.list.map(TodoCheckBox)}
         </ScrollView>
```

下記を実行して見た目が変わらないことを確認してください。

```sh
$ react-native run-ios
```

storeに入れているmessageの値を変えてreloadし直して画面に表示されるtodoが変わることを確認してください。

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/859b5d2da2ea07fb508b4b21b0c0d1f78427e09d

## アクションを作成する

テキストフォーム内の値を保持するアクションを作成します。

MobXの `@action` を使うことで実装できます。

```diff:src/stores/todo.js
-  import { observable } from 'mobx';
+  import { observable, action } from 'mobx';

+  @observable inputText = '';
+
+  @action onChangeText(message = '') {
+    this.inputText = message;
+  }
```

Componentに上記のアクションと保持した値を利用できるできるようにします。

```diff:src/components/index.js
+            <FormInput
+              autoCapitalize="none"
+              value={todo.inputText}
+              onChangeText={(message) => todo.onChangeText(message)}
+            />
```

ここまでの変更でテキストフォームに値を入力できれば成功です。
ほんとにできているか気になる方は下記のようにコンソールデバッグメッセージを仕込んで呼び出されていることを確認してください。

```diff:src/stores/todo.js
  @action.bound onChangeText(message = '') {
+   console.log('message:', message);
    this.inputText = message;
  }
```

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/6de06ed423f20eab7aa108f9f8a3bbb78f93a062

ストアをでテキストフォームの値を管理することができるようになったので管理している値でtodoを作成できるようにします。
ホントはサーバーサイド登録するべき、サンプルだから許される(魔法の言葉)

```diff:src/stores/todo.js
+  @action add() {
+    // 空文字の場合は登録しない
+    if (this.inputText === '') return;
+
+    this.list.push({
+      id: Math.random().toString(36).slice(-8),    // 雑にランダム文字列を生成
+      message: this.inputText,
+      isComplete: false,
+    });
+    this.inputText = '';
+  }
```

Componentに上記のアクションを利用できるできるようにします。

```diff:src/components/index.js
          <View style={{ flex: 5 }}>
            <FormInput
              autoCapitalize="none"
              value={todo.inputText}
              onChangeText={todo.onChangeText}
+             onSubmitEditing={() => todo.add()}
            />
          </View>
          <View style={{ flex: 1 }}>
            <Button
-             onPress={() => console.log('add')}
+             onPress={() => todo.add()}
              title="ADD"
            />
          </View>

```

ここまでの変更でテキストフォームにメッセージを入れてボタン押したタイミングでtodoが作成されれば成功です。
必要の亡くなったダミーデータを削除しましょう。

```diff:src/stores/todo.js
-  // TODO: とりあえずデータを入れておく
-  @observable list = [
-    { id: 1, message: 'todo!', isComplete: false, },
-    { id: 2, message: 'complete todo!', isComplete: true, },
-    { id: 3, message: 'complete todo!', isComplete: true, },
-  ];
+  @observable list = [];
```

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/2d901301496eec483065244b10278d0043dd7772

次にチェックボックスのトグルを実装します。リスト内部のオブジェクトのisCompleteを反転させることで実現できます。(もっと良い実装があったら教えてください)

```diff:src/stores/todo.js
+  // 指定したidのコンプリートフラグを反転する
+  @action toggleComplete(id) {
+    this.list = this.list.map(o => o.id === id ? { ...o, isComplete: !o.isComplete } : o);
+  }
```

Componentに上記のアクションを利用できるできるようにします。

```diff:src/components/index.js
-        onPress={() => console.log('todoトグル')}
+        onPress={() => todo.toggleComplete(row.id)}
```

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/65aed63e7608b736c8f101dc5a2f679e3a0fb2d6

最後にtodoを削除できるようにします。 `Array.filter` をつかって一致する `id` を消します。

```diff:src/stores/todo.js
+  @action del(id) {
+    this.list = this.list.filter(o => o.id !== id);
+  }
```

Componentに上記のアクションを利用できるできるようにします。

```diff:src/components/index.js
-              { text: '削除', onPress: () => console.log('todo削除') },
+              { text: '削除', onPress: () => todo.del(row.id) },
```

サンプルは下記です。

https://github.com/OshiroSeiya/AwesomeProject/commit/85d266205da7b68ea82bf6e3c3c7ebd83ea76a57

すべての機能が動くことを確認しましょう。

```sh
$ react-native run-ios
```

# 発展(時間が余った人用)

現状のアプリは閉じたらtodoが全部吹き飛びます。これを `AsyncStorage` 等に入れられるようにしてみましょう。生のAPIを利用するのは苦痛なのでいい感じにラップされている `react-native-store` を利用して見るといいかもしれません。

後ろが真っ白で見づらいので、backgroundColorを設定してみましょう。

todoが下に入っていくのに違和感を感じる人は最新の登録が上に来るようにしてみましょう。

# 最後に

ハンズオンはこれで終わりです。
Nativeの話よりReactとMobXの話が中心になってしまった感じはしますが、ネイティブが関係しないコードであればほぼweb開発の知識で開発を始めることができると感じたと思います。
まだメジャーバージョンが出ておらず辛いところもあります。そこは頑張って行きましょう！