Ruby on Rails と React Native で作る web & モバイルアプリ [モバイルアプリ編]

# イントロダクション

## 目的

この記事は、[Ruby on Rails と React Native で作る web & モバイルアプリ [webアプリ編]](https://qiita.com/OgiharaRyo/private/9949e5a2cda5e48060f0) の続編です。Ruby on Rails で作った web アプリケーションのリソースを API 経由で React Native アプリケーションから操作します。今回は以下のような web とモバイルの両方に対応したアイパス認証付きのシンプルなタスク管理システムを作っています。

<img width="347" alt="スクリーンショット 2019-12-13 11.25.13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/3a1b0c08-2262-6c99-bba1-5169a63604ab.png"><img width="400" alt="スクリーンショット 2019-12-13 11.26.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/3357ce83-5f8b-7873-bfd0-0925fd425e3f.png">

React Native の演習は行いたいけど Ruby on Rails 編には興味がない方は、Ruby on Rails アプリケーションを以下より clone して[デプロイ](https://qiita.com/OgiharaRyo/private/9949e5a2cda5e48060f0#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4)するか、今のところは [https://zone-web.herokuapp.com/](https://zone-web.herokuapp.com/) を生かしているので、こちらを利用してください。

|フレームワーク|リポジトリ|
|:---|:---|
|Ruby on Rails (web)|[https://github.com/ogihara-ryo/zone-web](https://github.com/ogihara-ryo/zone-web)|
|React Native (mobile)|[https://github.com/ogihara-ryo/zone-mobile](https://github.com/ogihara-ryo/zone-mobile)|


## 想定読者

私自身がまだモバイルアプリケーションを何もリリースしたことのない初学者であるため、初学者による初学者のための演習になっています。写経のみで進めていける演習になっていますが、多少のプログラミング全般の心得は必要になるかもしれません。私は最近 Ruby on Rails で作った web アプリを React Native アプリ対応しようと思っているのですが、この辺りの参考日本語記事が少なくて苦労しているため、同じような立場の人の助けになれれば嬉しく思います。ただ、筆者は前述の通り完全に素人なので、React Native のベストプラクティス等はまるで理解しておらず、とりあえずシンプルに動くものを作る、ぐらいのモチベーションでこの演習を作りました。そのため、技術的に正しくないコードや誤った説明が含まれている可能性があります。何かお気付きのことがありましたら編集リクエストを頂けると嬉しく思います。

もし、この記事を進めていく上で躓いた場合や理解できないことがあった場合は [@OgiharaRyo](https://twitter.com/OgiharaRyo) までご連絡頂くか、現在300人ほど参加している技術質問 slack コミュ二ティを運営していますので、こちらで質問して頂ければと思います。([slack 招待リンク](https://join.slack.com/t/it-newbie/shared_invite/enQtODQ3MTE0MjM0MzM3LTI2ZmM2ODMzYmViYzY3ODBlM2VhZGRhYWE1NTc0ZmFkZTI2OWIwYmE5MTRlYWU5NTY3OTNjYjJhMTlkNmVhNDI))

## 環境

2019年12月初旬執筆時点の最新版を使用します。

```zsh
% node -v
v13.3.0

% npm -v
6.13.2

% watchman -v
4.9.0

% expo-cli -V
3.10.2
```

インストールに関しては先人の知恵と功績によって特に癖はないと思いますが、Mac で Homebrew が入っている環境であれば以下で大体いけます。いけなかったら何とかしてください。

```zsh
% brew install node
% npm install npm@latest -g
% brew install watchman
% npm install expo-cli --global
```

# React Native アプリケーションの開発

## expo init

早速アプリケーションを作っていきましょう。まずは空っぽの React Native アプリケーションを `expo init` で作成します。`Choose a template` と言われるので `blank` のまま return キーで進みます。

```zsh
% expo init zone-mobile          
? Choose a template: (Use arrow keys)
  ----- Managed workflow -----
❯ blank                 a minimal app as clean as an empty canvas 
  blank (TypeScript)    same as blank but with TypeScript configuration 
  tabs                  several example screens and tabs using react-navigation 
  ----- Bare workflow -----
  minimal               bare and minimal, just the essentials to get you started 
  minimal (TypeScript)  same as minimal but with TypeScript configuration 
```

次に `<The name of your app visible on the home screen>` とホームスクリーンに表示するアプリケーションの名前を尋ねられます。ここでは `Zone` と入力しておきましょう。

```zsh
? Please enter a few initial configuration values.
  Read more: https://docs.expo.io/versions/latest/workflow/configuration/ › 50% completed
 {
   "expo": {
     "name": "<The name of your app visible on the home screen>",
     "slug": "zone-mobile"
   }
 }
```

## 起動確認

早速このまま起動してみましょう。

```zsh
% expo start
```

ブラウザーに [localhost:19002](http://localhost:19002) が立ち上がります。画面左下に注目してください。

<img width="288" alt="スクリーンショット 2019-12-08 2.03.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/443fcbf4-e7ef-070c-4531-6e6abb494035.png">

開発中アプリケーションの動作確認方法は大きく分けると、シミュレーターを使う方法と実機を使う方法の2つがあります。Xcode と Command Line Tools をインストール済みのマシンであれば、`Run on iOS simulator` を選択するとシミュレーターが起動します。シミュレーターから Expo アプリケーションを起動してよしなに操作すれば下記のような画面が表示されます。

<img width="400" alt="スクリーンショット 2019-12-08 2.02.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/46c3e748-7a01-98d6-80b7-2467024386a7.png">

実機を使う場合は、お手元のスマホでストアから Expo Client をインストールして、[localhost:19002](http://localhost:19002) に表示されているあなた専用の QR コードを読み込めば Expo Client アプリが起動して上記のような画面が表示されます。ちなみに私は通常 iPhone と Pixel の2台の実機を使いながら iOS と Android の両方を並行して動作確認しながら開発しています。

## react-navigation

今回開発するアプリケーションは2つの画面を持ちます。1つはログインフォームを持つログイン画面、もう1つはタスク管理を行うメイン画面です。開発のはじめの第一歩としては、ログイン画面とメイン画面を用意して、お互いを行き来できるような実装を行います。前準備として、[react-navigation](https://github.com/react-navigation/react-navigation) をインストールします。`react-navigation` は画面間の遷移をハンドリングしてくれるライブラリです。[ドキュメント](https://reactnavigation.org/docs/en/getting-started.html) に従って [react-navigation](https://github.com/react-navigation/react-navigation) 及びその依存ライブラリ [react-native-gesture-handler](https://github.com/kmagiera/react-native-gesture-handler), [react-native-reanimated](https://github.com/software-mansion/react-native-reanimated), [react-native-screens](https://github.com/kmagiera/react-native-screens) を `expo install` します。

```zsh
% expo install react-navigation react-native-gesture-handler react-native-reanimated react-native-screens
```

それではナビゲーションを実装していきます。今回は、`App.js` にナビゲーションを置いて、`src/screens/Login.js` と `src/screens/Main.js` を互いに遷移できるようなコードを書いていきます。まずはログイン画面とメイン画面を用意します。ただお互いの画面に遷移するためのボタンが1つだけ置いてある簡素な画面です。`Button` の `onPress` イベントで `this.props.navigation.navigate('main')` のようなメソッドコールを行っているのがポイントです。

```jsx:src/screens/Login.js
import React from 'react';
import { View, Button } from 'react-native';

export default class Login extends React.Component {
  render() {
    return (
      <View>
        <Button
          title="ログイン"
          onPress={() => {this.props.navigation.navigate('main')}}>
        </Button>
      </View>
    );
  }
}
```

```jsx:src/screens/Main.js
import React from 'react';
import { View, Button } from 'react-native';

export default class Main extends React.Component {
  render() {
    return (
      <View>
        <Button
          title="ログアウト"
          onPress={() => {this.props.navigation.navigate('login')}}>
        </Button>
      </View>
    );
  }
}
```

作った2つの画面をナビゲーターにセットします。今回は、`createSwitchNavigator` を使用します。

```jsx:App.js
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { createAppContainer, createSwitchNavigator } from 'react-navigation';

import Login from './src/screens/Login'
import Main from './src/screens/Main'

export default function App() {
  const MainNavigator = createAppContainer(
    createSwitchNavigator({
      login: { screen: Login },
      main: { screen: Main }
    })        
  )

  return (
    <View style={styles.container}>
      <MainNavigator />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
```

これで2画面を遷移できるアプリケーションになりました。

<img width="300" alt="スクリーンショット 2019-12-08 9.51.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/e22e24d1-fa25-dbb1-c36e-fbc02194e65a.png"><img width="300" alt="スクリーンショット 2019-12-08 9.51.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/bb86035d-1b97-379c-7b2d-5cd77d8daebc.png">

## ログイン

それでは、本格的にログインフォームを実装していきましょう。解説は後にして、一旦全てのコードを一気に実装してしまいましょう。記事内の web アプリケーションの URL は、ご自身のアプリケーションをデプロイされていたら差し替えてください。

```jsx:src/screens/Login.js
import React from 'react';
import { View, Text, Button, TextInput, ActivityIndicator, StyleSheet } from 'react-native';

export default class Login extends React.Component {
  constructor(props) {
    super(props);
    this.state = { accountId: '', password: '', loading: false, failed: false };
  }

  onSubmit() {
    this.setState({ loading: true })
    return (
      fetch(`https://zone-web.herokuapp.com/api/login.json?account_id=${this.state.accountId}&password=${this.state.password}`)
        .then((response) => response.json())
        .then((jsonData) => {
          this.setState({ loading: false })
          if (jsonData['api_token']) {
            this.props.navigation.navigate('main')
          }
          else {
            this.setState({ failed: true })
          }
        })
        .catch((error) => console.error(error))
    )
  }

  loginButton() {
    if (this.state.loading) {
      return <ActivityIndicator size="small" />
    }
    else {
      return <Button title="ログイン" onPress={() => {this.onSubmit()}} />
    }
  }

  render() {
    return (
      <View>
        {this.state.failed && <Text>ログインに失敗しました。</Text>}

        <TextInput
          style={styles.textInput}
          placeholder="アカウントID"
          onChangeText={(accountId) => this.setState({accountId})}
        />

        <TextInput
          secureTextEntry={true}
          style={styles.textInput}
          placeholder="パスワード"
          onChangeText={(password) => this.setState({password})}
        />

        {this.loginButton()}
      </View>
    );
  }
}

const styles = StyleSheet.create({
  textInput: {
    height: 60,
    width: 300,
    paddingLeft: 20,
    margin: 10,
    borderWidth: 1,
    borderRadius: 8,
  }
});
```

まずは動かしてみましょう。誤ったアカウントIDやパスワードでログインを試みたらエラーメッセージが表示され、正しいアカウントIDとパスワードでログインを試みたらメイン画面へ遷移することを確認してください。

<img width="300" alt="スクリーンショット 2019-12-08 16.06.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/b5749b5f-df15-d5b7-6a2b-29828c891098.png">

さて、今回実装したのは比較的長いコードではありますが、この `Login` コンポーネントが何をしているかは、各 `state` がどのタイミングで更新されて、何に使われているのかを知ることで全容を把握することができます。コードと読み比べてみましょう。

|state|更新タイミング|用途|
|:---|:---|:---|
|accountId|アカウントIDの `TextInput` にユーザーが入力する度に `onChangeText` イベントで現在の値がセットされます。|web アプリケーションのログイン API に与えるパラメーターとしてセットします。|
|password|パスワードの `TextInput` にユーザーが入力する度に `onChangeText` イベントで現在の値がセットされます。|web アプリケーションのログイン API に与えるパラメーターとしてセットします。|
|loading|web アプリケーションに `fetch` でリクエストを送る前に `true` にし、レスポンスを受け取った後に `false` にします。|ログインボタンが押された後、web アプリケーションと通信している間にログインボタンを `ActivityIndicator` (いわゆるロード中のクルクル)に差し替えることで、ユーザーに通信中であることを伝えると同時に、ログインボタンを不要に連打されないようにします。|
|failed|ログイン API を叩いて API トークンが返ってこなかった場合はアカウントIDかパスワードが間違っていたとして `true` にします。|true の場合にログインに失敗した旨をユーザーに通知します。|


### APIトークンの保存とログインの永続化

ここまでのコードでは、APIトークンが返ってきた場合はログインに成功したとしてナビゲーターをメイン画面に変更していますが、このAPIトークンは後のタスク操作 API をコールするために使用するのでちゃんと保存しておく必要があります。また、API トークンを一度受け取ってしまえば次回以降のアプリケーション起動時はログイン画面を挟まずにメイン画面を表示した方が親切です。今回は `AsyncStorage` というキーバリューストレージシステムに API トークンを保存します。

```diff:src/screens/Login.js
  import React from 'react';
- import { View, Text, Button, TextInput, ActivityIndicator, StyleSheet } from 'react-native';
+ import { View, Text, Button, TextInput, ActivityIndicator, StyleSheet, AsyncStorage } from 'react-native';

  export default class Login extends React.Component {
    constructor(props) {
      super(props);
      this.state = { accountId: '', password: '', loading: false, failed: false };
    }
+
+   async componentDidMount() {
+     if (await AsyncStorage.getItem('api_token')) {
+       this.props.navigation.navigate('main')
+     }
+   }

    onSubmit() {
      this.setState({ loading: true })
      return (
        fetch(`https://zone-web.herokuapp.com/api/login.json?account_id=${this.state.accountId}&password=${this.state.password}`)
          .then((response) => response.json())
          .then((jsonData) => {
            this.setState({ loading: false })
            if (jsonData['api_token']) {
+             AsyncStorage.setItem('api_token', jsonData['api_token']);
              this.props.navigation.navigate('main')
            }
            else {
              this.setState({ failed: true })
            }
          })
          .catch((error) => console.error(error))
      )
    }
```

`componentDidMount` は、コンポーネントがツリーに挿入された直後に呼び出されます。この時、`AsyncStorage` に既に API トークンが入っていた場合はナビゲーターをメイン画面に切り替えています。

### ログアウト

最後に、ログアウトの実装も行いましょう。これはシンプルで、`AsyncStorage` から API トークンを削除してナビゲーターをログイン画面へ切り替えるだけです。

```diff:src/screens/Main.js
  import React from 'react';
- import { View, Button } from 'react-native';
+ import { View, Button, AsyncStorage } from 'react-native';

  export default class Main extends React.Component {
+   logout() {
+     AsyncStorage.removeItem('api_token');
+     this.props.navigation.navigate('login')
+   }

    render() {
      return (
        <View>
          <Button
            title="ログアウト"
-           onPress={() => {this.props.navigation.navigate('login')}}>
+           onPress={() => {this.logout()}}>
          </Button>
        </View>
      );
    }
  }
```

## タスクの CRUD

各処理の雑な解説は後回しにして、まずは完成形です。アクションも DOM のレンダリングも全て同じファイルにまとめているので100行ほどあります。

<img width="400" alt="スクリーンショット 2019-12-13 11.26.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102542/3357ce83-5f8b-7873-bfd0-0925fd425e3f.png">

```jsx:src/screens/Main.js
import React from 'react';
import { View, FlatList, TextInput, Button, AsyncStorage, ActivityIndicator, StyleSheet } from 'react-native';
import { CheckBox } from 'react-native-elements';

export default class Main extends React.Component {
  constructor(props) {
    super(props);
    this.state = { taskName: '', tasks: [], loading: '', apiToken: '' };
  }

  async componentDidMount() {
    this.setState({ loading: true, apiToken: await AsyncStorage.getItem('api_token') })
    fetch(`https://zone-web.herokuapp.com/api/tasks.json?api_token=${this.state.apiToken}`)
      .then((response) => response.json())
      .then((jsonData) => (this.setState({ loading: false, tasks: jsonData })))
      .catch((error) => console.error(error));
  }

  submitCreateTask() {
    if (!this.state.taskName) return

    this.setState({ loading: true })
    fetch(`https://zone-web.herokuapp.com/api/tasks`, {
      method: 'POST',
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({ api_token: this.state.apiToken, task: { name: this.state.taskName } })
    })
    .then(response => response.json())
    .then(json => { this.setState({ tasks: this.state.tasks.concat(json), taskName: '', loading: false }) })
    .catch((error) => console.error(error));
  }

  changeFinished(item) {
    item.finished = !item.finished
    this.setState({ loading: true })

    fetch(`https://zone-web.herokuapp.com/api/tasks/${item.id}`, {
      method: 'PATCH',
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({ api_token: this.state.apiToken, task: { finished: item.finished } })
    })
    .then(response => this.setState({ loading: false }))
    .catch((error) => console.error(error));
  }

  createTaskButton() {
    if (this.state.loading) return <ActivityIndicator size="small" />
    else return <Button title="作成" onPress={() => {this.submitCreateTask()}} />
  }

  renderTasks() {
    if (this.state.loading) return <FlatList />
    else {
      return(
        <FlatList
          data={this.state.tasks}
          keyExtractor={(item) => item.id.toString()}
          renderItem={({ item }) => (
            <CheckBox title={item.name} checked={item.finished} onPress={() => this.changeFinished(item)} />
          )}
        />
      )
    }
  }

  logout() {
    AsyncStorage.removeItem('api_token');
    this.props.navigation.navigate('login')
  }

  render() {
    return (
      <View>
        <View style={styles.form}>
          <TextInput
            style={styles.textInput}
            placeholder="タスク名"
            value={this.state.taskName}
            onChangeText={(taskName) => this.setState({taskName})}
          />

          {this.createTaskButton()}
        </View>

        {this.renderTasks()}

        <View style={styles.logout}>
          <Button title="ログアウト" onPress={() => {this.logout()}} />
        </View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  form: { margin: 40 },
  textInput: {
    height: 60,
    width: 300,
    paddingLeft: 20,
    margin: 10,
    borderWidth: 1,
    borderRadius: 8,
  },
  logout: { marginBottom: 20 }
});
```

`react-native-elements` の `CheckBox` を利用するのでインストールします。

```zsh
% expo install react-native-elements
```

### state

まずはログイン画面と同様に `Main` コンポーネントの持つ各 `state` の動きを把握しましょう。 

|state|更新タイミング|用途|
|:---|:---|:---|
|taskName|タスク作成フォームの `TextInput` にユーザーが入力する度に `onChangeText` イベントで現在の値がセットされます。また、タスク作成ボタンを押した時に `TextInput` から現在の値を空にするために空文字列をセットします。|web アプリケーションのタスク作成 API に与えるパラメーターとしてセットします。|
|tasks|`componentDidMount` で web アプリケーションのタスク一覧 API からタスク情報を取得してセットします。また、各タスクの完了状態のチェックボックスを操作した時に、該当のタスクの `finished` の値をチェックボックスの状態に応じて変更します。|タスク一覧の表示に使います。|
|loading|ログイン画面と同様、web アプリケーションに `fetch` でリクエストを送る前に `true` にし、レスポンスを受け取った後に `false` にします。|ログイン画面と同様、タスク一覧の取得、タスクの作成、タスクの完了状態の更新の API をコールした後、web アプリケーションと通信している間に各種 UI を `ActivityIndicator` に差し替えることで、ユーザーに通信中であることを伝えると同時に、不要な操作をされないようにします。この挙動は UX を損ねるので気に入らなければ外してしまってユーザーに通信していることを意識させないようにしても良いでしょう。今回は web アプリケーションとやり取りしていることを明示するために操作の度にクルクルが表示されます。|
|apiToken|`componentDidMount` で `AsyncStorage` から API トークンを取り出してセットします。|毎回 `await AsyncStorage.getItem('api_token')` してストレージアクセスするのは非効率なので、一度 API トークンを取得したら `state` にキャッシュしています。|

### 表示

まずはタスク一覧の表示についてです。実際の表示部分は下記です。

```jsx
  renderTasks() {
    if (this.state.loading) return <FlatList />
    else {
      return(
        <FlatList
          data={this.state.tasks}
          keyExtractor={(item) => item.id.toString()}
          renderItem={({ item }) => (
            <CheckBox title={item.name} checked={item.finished} onPress={() => this.changeFinished(item)} />
          )}
        />
      )
    }
  }
```

ロード中(web アプリケーションとの通信中)は空っぽの `FlatList` を表示し、ロード中でない場合は `tasks` に従ってアイテムを表示しています。React Native では、コンポーネントの `state` が更新された時にそのコンポーネントの `render` メソッドが実行されます。つまり、`loading` の `state` が更新される度にこのコンポーネントの `render()` が走るため、`if` に入ってきたり `else` に入ってきたりするわけですね。この `FlatList` には `this.state.tasks` を与えて `CheckBox` をレンダリングしていますが、`this.state.tasks` に値が入るのは以下の処理です。

```jsx
  async componentDidMount() {
    this.setState({ loading: true, apiToken: await AsyncStorage.getItem('api_token') })
    fetch(`https://zone-web.herokuapp.com/api/tasks.json?api_token=${this.state.apiToken}`)
      .then((response) => response.json())
      .then((jsonData) => (this.setState({ loading: false, tasks: jsonData })))
      .catch((error) => console.error(error));
  }
```


タスクの一覧は `componentDidMount` で行っています。`componentDidMount` は、`render` メソッドによる DOM 挿入直後に呼び出されます。このコンポーネントでは `componentDidMount` で web アプリケーションからタスク一覧を取得しているのでタスク一覧表じまでの順序としては以下のようになります。

1. 初回の `render` で 空の `FlatList` を表示
2. `componentDidMount` で API からタスクの一覧を取得
3. 取得後に `loading` と `tasks` の `state` が変更されたことで再度 `render` が実行されてタスク一覧を表示


### 作成

続いてタスクの作成機能について見ていきましょう。`render` メソッド内のタスク作成フォーム部分は以下です。

```jsx
        <View style={styles.form}>
          <TextInput
            style={styles.textInput}
            placeholder="タスク名"
            value={this.state.taskName}
            onChangeText={(taskName) => this.setState({taskName})}
          />

          {this.createTaskButton()}
        </View>
```

今までと同様、ロード中は `ActivityIndicator` のクルクルを、ロード中でない場合は作成ボタンをレンダリングします。

```jsx
  createTaskButton() {
    if (this.state.loading) return <ActivityIndicator size="small" />
    else return <Button title="作成" onPress={() => {this.submitCreateTask()}} />
  }
```

作成ボタンの `onPress` イベントで呼ばれるのが以下です。タスク名の入力フィールドに何も入力されていなかった場合は何もせずに終了し、何か入力されていた場合は web アプリケーションのタスク作成 API をコールしています。レスポンスを受けたら、`tasks` の `state` に今追加したタスクの情報を詰めて、`taskName` を空にすることでタスク入力フォームのタスク名入力フィールド(`TextInput`)を空にしています。

```jsx
  submitCreateTask() {
    if (!this.state.taskName) return

    this.setState({ loading: true })
    fetch(`https://zone-web.herokuapp.com/api/tasks`, {
      method: 'POST',
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({ api_token: this.state.apiToken, task: { name: this.state.taskName } })
    })
    .then(response => response.json())
    .then(json => { this.setState({ tasks: this.state.tasks.concat(json), taskName: '', loading: false }) })
    .catch((error) => console.error(error));
  }
```

### 更新

最後はチェックボックスの状態を変更した時の完了状態更新処理です。チェックボックスのレンダリング部分は、タスク一覧を表示した時の `FlatList` の各アイテムの中にあります。

```jsx
  renderTasks() {
    if (this.state.loading) return <FlatList />
    else {
      return(
        <FlatList
          data={this.state.tasks}
          keyExtractor={(item) => item.id.toString()}
          renderItem={({ item }) => (
            <CheckBox title={item.name} checked={item.finished} onPress={() => this.changeFinished(item)} />
          )}
        />
      )
    }
  }
```

`CheckBox` の `onPress` イベントで呼ばれるのが以下です。引数の `item` は参照渡しなので、`item.finished` の論理を反転すると `tasks` の `state` が更新されます。

```jsx
  changeFinished(item) {
    item.finished = !item.finished
    this.setState({ loading: true })

    fetch(`https://zone-web.herokuapp.com/api/tasks/${item.id}`, {
      method: 'PATCH',
      headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
      body: JSON.stringify({ api_token: this.state.apiToken, task: { finished: item.finished } })
    })
    .then(response => this.setState({ loading: false }))
    .catch((error) => console.error(error));
  }
```

以上で React Native の演習は終了です。

# 終わりに

今回は2記事構成にて Ruby on Rails アプリケーションと React Native アプリケーションの連携を行いました。冒頭にも書きましたが、技術的に正しくないコードや誤った説明があったかもしれないので、何かお気付きのことがありましたら編集リクエストを頂けると嬉しく思います。React Native アプリケーション開発の日本語入門記事は Firebase を使ってのものが多く、既存の web アプリケーションに API を生やしつつモバイル対応するようなシチュエーションを想定した演習は少なく思えたので、本記事がどなたかの役に立てば嬉しく思います。

Ruby on Rails Advent Calendar 17日目は [canecco](https://qiita.com/canecco) さんの「ローカル通知に画像を表示する話」です。