React Native + Expo アプリで unstatedのStateにaxiosを使って簡易テストサーバーからデータ取得

この記事は、「[【連載】初めてのReact Native + Expo開発環境構築入門](https://qiita.com/hitotch/items/5142fff638c7805d84d5)」の子記事です。環境などの条件は、親記事をご覧ください。

---

　[前回](https://qiita.com/hitotch/items/3bba68417af561b1a4d0)までに、unstatedに格納した請求書情報をきれいに画面上にリスト表示できるようになったので、今回は請求書情報をサーバーから取得できるようにします。テスト用に簡易ローカルHTTPサーバーも立ち上げます。

目標
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/a771cd39-dbe1-2085-58b3-66bbf10db979.png)


# サーバーとの通信は、Reduxよりunstatedが楽

　Reduxを使った場合、そもそもそのままではサーバーからデータを取得してグローバルStateに取得データを格納できません。（厳密にはできますが、各コンポーネント内に非同期処理を書かなければいけないので、コンポーネント間でコード再利用ができない。）なので通常[redux-thunk](https://www.npmjs.com/package/redux-thunk)というミドルウェアを使って``Action``で非同期処理を実行できるようにします。これに比べて、unstatedなら2つの点で断然有利です。

- ミドルウェア無しで(追加モジュール無しで)非同期処理できる
- Containerのメンバーメソッドとして非同期処理を書ける。ActionやReducerなどあちこちに処理を書かなくていい

※もちろんReduxのほうがいい場合もあります。

# テストサーバーを立てる

　実際のコーディングの前に、まずはテストサーバーを立てましょう。ローカルPCにnode.jsのサーバーを立てて、そこにinvoice.jsというファイルを置いて、HTTP通信で取得できるようにします。今回はnode.jsのサーバーを簡易的に立ててくれる[http-server](https://www.npmjs.com/package/http-server)を使います。

まず``http-server``をグローバルインストール。

```
npm install -g http-server
```

　次に、テストサーバーのドキュメントルートフォルダを設置。プロジェクトフォルダの横とかがわかりやすいかもですね。
　たとえば今回のプロジェクトは私の場合``C:\ExpoProjects\hello-world-test``に設置しているので、``C:\ExpoProjects\hello-world-testdata``というフォルダを作って、以下の``inovice.js``を設置します。

```json:invoice.js
{"customers":[{"id":0,"name":"ABC Store","addr1":"123 Abc St.","addr2":"","city":"New York","state":"NY","zip":"10001"},{"id":1,"name":"123 Deli","addr1":"1 Def Ave.","addr2":"","city":"New York","state":"NY","zip":"10002"},{"id":2,"name":"Xyz mart","addr1":"23 Xyz Blvd.","addr2":"","city":"New York","state":"NY","zip":"10003"},{"id":3,"name":"Xyz2 mart","addr1":"1 Xyz Blvd.","addr2":"","city":"New York","state":"NY","zip":"10004"}],"products":[{"id":0,"name":"Blue ribbon","shortName":"B.R.","price":10.5,"cost":7.2},{"id":1,"name":"Red ribbon","shortName":"R.R.","price":9.5,"cost":6},{"id":3,"name":"White shirt","shortName":"W.S.","price":15,"cost":9.3}],"invoices":[{"id":0,"date":"2/2/2019","customer":0,"items":[{"product":0,"qty":5,"adjust":0,"credit":0},{"product":1,"qty":3,"adjust":0,"credit":0},{"product":2,"qty":4,"adjust":0,"credit":0}]},{"id":1,"date":"2/2/2019","customer":0,"items":[{"product":0,"qty":7,"adjust":0,"credit":0}]},{"id":2,"date":"2/2/2019","customer":3,"items":[{"product":0,"qty":5,"adjust":0,"credit":0},{"product":1,"qty":3,"adjust":0,"credit":0},{"product":2,"qty":4,"adjust":0,"credit":0}]},{"id":3,"date":"2/2/2019","customer":2,"items":[{"product":0,"qty":5,"adjust":0,"credit":0},{"product":1,"qty":3,"adjust":0,"credit":0},{"product":2,"qty":4,"adjust":0,"credit":0}]}]}
```

　長いのでコンパクト化していますが、中身が気になる方は、[オンラインJSONエディタ](https://jsoneditoronline.org/)に貼り付けてみてください。このデータにはInvoiceが4つ入っています。

　ではローカルサーバーを立てます。PowerShellを開き(Visual Studio Codeの中ではなく、独立したPowerShellを立ち上げます)、ドキュメントルートまで移動して``http-server``を実行。

```
cd \ExpoProjects\hello-world-testdata
http-server
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/91865256-51e0-af95-c041-fd4c54c553c7.png)

　上のようにどこに立ち上がってるか表示してくれるので、実験に使ってるモバイル実機のブラウザでアクセスしてみます。このとき、ポート番号とファイル名を忘れないように注意します。

<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/607d7a51-f03c-8240-dca5-de90c425a8c7.png">　<img width="375" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/f8979d80-ffe7-655c-2e62-deff4a7fa471.png">

# axiosをインストール

　axiosはHTTP通信を「よしなに」処理してくれる便利屋さんです。なくてもいいですが、記述が簡単になるのでぜひ。

```
npm install axios
```

# unstatedコンテナに通信用メソッドを追加

　[以前の記事](https://qiita.com/hitotch/items/36dcc678976bd4ebe006)で作ったunstatedコンテナ``containers/InvoiceContainer.js``に、API通信を実行するメソッドを追加します。

```jsx:/containers/InvoiceContainer.js
import axios from 'axios';
const INVOICE_API_ENDPOINT = 'http://192.168.1.8:8080/invoice.js';
...
export default class InvoiceContainer extends Container {
  constructor(props = {}) {
    super();
    this.state = {
      data: props.initialSeeding ? Seeder.getSeed() : this.getEmptyData(),
      isDataLoading: false
    };
  }
...
  getDataFromServer() {
    this.setState({ isDataLoading: true });
    axios
      .get(INVOICE_API_ENDPOINT, { params: {} })
      .then(results => {
        console.log("HTTP Request succeeded.");
        console.log(results);
        this.setState({ data: results.data });
        this.setState({ isDataLoading: false });
      })
      .catch(() => {
        console.log("HTTP Request failed.");
        this.setState({ isDataLoading: false });
      });
  }
...
}
```
　``INVOICE_API_ENDPOINT``には、簡易テストサーバーのアドレスに対象データファイル名まで入れたURLを指定します。

　グローバルStateとして、``isDataLoading``を追加しました。axiosがサーバーと通信中にこれをオンにすることで、画面コンポーネント側でこのフラグを見て表示内容を変更できることを狙っています。

　追加した``getDataFromServer()``では、axiosによるサーバーとの通信が実装されています。最初にロード中フラグ``isDataLoading``を立てて、``INVOICE_API_ENDPOINT``と通信し、成功（``.then``）したらグローバルStateの``data``に書き込んでからロード中フラグオフ、失敗（``.catch``）したら単にロード中フラグオフ。

　これだけで通信コーディングは終わりです。実践ではエラーになった場合の処理についてもう少し書く必要がありますね。

# API通信を開始するボタンを作る

　単純に、画面コンポーネントに``Import``ボタンとイベントを実装します。

```jsx:HomeScreen.js
class HomeScreenContent extends React.Component {
  constructor(props) {
    super(props);
    this.onImportClick = this.onImportClick.bind(this);
  }

  onImportClick() {
    this.props.globalState.getDataFromServer();
  }
...
  render() {
    let globalState = this.props.globalState;
    let invoiceList = <Text>No invoice</Text>;

    // Wait for data loading...
    if (globalState.state.isDataLoading) {
      return (
        <View>
          <Text>loading...</Text>
        </View>
      );
    }
...
    return (
      <Container>
        <Content>
          <View style={{ flexDirection: "row" }}>
            <Left>
              <Button light onPress={() => this.onImportClick()}>
                <Icon type="FontAwesome5" name="file-import" />
                <Text style={{ paddingLeft: 0 }}>Import</Text>
              </Button>
            </Left>
            <Right>
              <Button onPress={() => this.props.navigation.navigate("Summary")}>
                <Icon type="FontAwesome5" name="poll-h" />
                <Text style={{ paddingLeft: 0 }}>Summary</Text>
              </Button>
            </Right>
          </View>

          {invoiceList}

          <View style={{ flexDirection: "row" }}>
            <Left>
              <Button
                onPress={() => this.props.navigation.navigate("InvoiceEdit")}
              >
                <Icon type="FontAwesome5" name="file-invoice-dollar" />
                <Text style={{ paddingLeft: 0 }}>InvoiceEdit</Text>
              </Button>
            </Left>
          </View>
        </Content>
      </Container>
    );
  }
}
...
```

　まずイベントハンドラ``onImportClick()``を登録します。この中では、グローバルStateコンテナに書いた``getDataFromServer``を呼んでるだけです。
　次に、``render()``内に、通信中（グローバルStateのisDataLoadingがオン）だったら「loading...」と表示するコードを入れます。これにより、通信中にほかの操作をされることを防ぎます。
　最後に、Importボタンを追加します。タップされたら``this.onImportClick()``を呼ぶだけです。
　※さらに若干ボタンの配置を変えています。

　修正が終わったら、テスト用サーバーがPowerShell上で起動していることを確認して、モバイル実機でテストします。結果は、以下のようになります。

初期画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/eb13d209-eec5-3b24-f5b8-c94b808aa740.png)

Importボタンを押した後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/315589/0aaf95b3-1ad5-af19-c264-eaf8333c4608.png)

　いかがでしょうか。Redux＋Thunk＋axiosを使ったことがある方は、あまりの簡単さに驚きが隠せないのでは？私はよっぽど大きなプロジェクトでない限り、もうReduxに戻りたくないです。。。