【ハンズオン資料】NCMBのReact Native SDKを使ってTodoアプリを作る

## コードについて

今回のコードは[NCMBMania/HandsOn_TodoApp_ReactNative: React Native SDKを用いたTodoアプリのハンズオン用ベースアプリです](https://github.com/NCMBMania/HandsOn_TodoApp_ReactNative)にアップロードしてあります。また、Expo Snackでも使用できます。

[NCMB-Expo-Todo-App - Snack](https://snack.expo.dev/@goofmint/1706f4)

## 利用技術について

今回は次のような組み合わせになっています。

- React Native 42.0.0
- Expo 42.0.1
- [NCMB React Native SDK](https://github.com/NCMBMania/ncmb-react-native) 2.0.13

## Expo Snackの利用

Expo Snackを使うことで、Webブラウザベースで開発できます。ユーザ登録後、一覧画面から新しいSnackを作成します。

https://expo.dev/snacks

そして、GitHubリポジトリからベースになるコードを取り込みます。

```
https://github.com/NCMBMania/HandsOn_TodoApp_ReactNative
```

## 仕様について

今回はシンプルにするために認証は利用していません。複数人で利用される場合などは、ID/パスワード認証を利用すると良いでしょう。

## 利用する機能について

Todoアプリで利用するNCMBの機能は次の通りです。

- データストア
  - Todoデータの保存
  - Todoデータの更新
  - Todoデータの削除

## 画面について

今回は2つの画面、6つのコンポーネントを利用しています。

### HomeScreen

Todoの一覧表示、新規作成を行う画面です。

![FireShot Capture 097 - タスクの編集 - localhost 20211011161343 - 097.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/14cc104e-b61d-0d4f-92ba-ad91cab1c2a3.jpeg)


```js
const HomeScreen = ({ navigation }) => {
  return (
    <View style={styles.container}>
      <Text style={styles.paragraph}>
        タスクを追加、編集してください
      </Text>
      <Card>
        <Home navigation={navigation} />
      </Card>
    </View>
  );
}
```

#### Homeコンポーネント

タスクの一覧表示を行います。

```js
// 描画
return (
  <View style={styles.container}>
    <List.Section>
      <List.Subheader>
        <View style={{ flexDirection: 'row'}}>
          <Text>既存のタスク</Text>
          <View
            style={{flexDirection: 'row', justifyContent: 'flex-end' }}
          >
            <ShowDelete />
          </View>
        </View>
      </List.Subheader>
      <FlatList
        data={ary}
        renderItem={({index, item }) => <TodoItem
          item={item}
          index={index}
          navigation={navigation}
          updated={updated}
        />}
        keyExtractor={item => item.get('objectId')}
      />
    </List.Section>      
    <InputTodo added={added} />
    <Text>{ show }</Text>
    <Banner
      message={message}
      show={show}
    />
  </View>
);
```

#### ShowDeleteコンポーネント

![FireShot Capture 097 - タスクの編集 - localhost 20211011161344 - 097.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/6052c826-7bf4-82cc-7ba1-acb647ddc147.jpeg)


タスクを一つでも選択（終了状態）にすると、ボタンを押せる状態にします。

```js
const ShowDelete = () => {
  return (
    <Button
      icon="delete"
      mode="contained"
      onPress={deleteTask}
      // 1件でも完了したタスクがあれば、削除を表示
      disabled={!(ary.filter(item => item.get('checked')).length > 0)}
    >
      <Text>削除</Text>
    </Button>
  )
};
```

#### Inputコンポーネント

タスクの入力と、新規作成を行うコンポーネントです。

```js
return (
  <View style={styles.container}>
    <TextInput
      label="タスク"
      mode="flat"
      dense="true"
      style={styles.textInput}
      value={text}
      onChangeText={(text) => setText(text)}
      onKeyPress={keyPress}
    />
  </View>
);
```

#### TodoItemコンポーネント

リスト表示時の一行あたりのコンポーネントです。

```js
// リスト（各行）の表示処理
return (<List.Item
  title={item.get('body')}
  // リストをタップしたら、編集画面に遷移する
  onPress={() => navigation.push('Detail', { index, item, updated })}
  // タスクの完了状態を見てチェックボックスを変更
  left={props => <Checkbox
    status={item.get('checked') ? 'checked' : 'unchecked'}
    // チェックボックスをタップしたら、完了状態を反転
    onPress={() => checked({index, item})}
    />
  }
  // > を表示
  right={props => <List.Icon
      icon="chevron-right"        
    />
  }
/>)
```

### EditScreen

![FireShot Capture 097 - タスクの編集 - localhost 20211011161345 - 097.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/197026/aa085977-0795-bbb4-f6bc-79894aed144e.jpeg)


Todoの編集を行う画面です。

```js
// 編集画面用
const EditScreen = (props) => {
  return (<View style={styles.container}>
      <Text style={styles.paragraph}>
        タスクを編集してください
      </Text>
      <Card>
        <Edit {...props} />
      </Card>
    </View>
  )
}
```

#### Editコンポーネント

選択されたタスクを表示、編集するコンポーネントです。

```js
return (
  <View style={styles.container}>
    <TextInput
      label="タスク"
      mode="flat"
      dense="true"
      value={text}
      style={styles.textInput}
      onChangeText={(text) => setText(text)}
      onKeyPress={keyPress}
    />
  </View>
);
```

### 追加のコンポーネント

追加としてcomponents/Banner.jsを作成します。これは、Snackbarコンポーネントを使って処理結果の通知に利用します。

```js
return (
  <Snackbar
    visible={visible}
    onDismiss={() => setVisible(false)}
    duration="2000"
    action={{
      label: 'OK',
      onPress: () => setVisible(false)
    }}>
    {message}
  </Snackbar>
);
```

## NCMBの初期化（実装済み）

React Native用のSDKを読み込みます。

```
npm i ncmb-react-native
```

### ncmb.jsの作成（実装済み）

componentsの中に ncmb.js を作成します。ここではReact Native SDKを読み込み、初期化を行います。

```js
import NCMB, { NCMBObject, NCMBQuery, NCMBUser, NCMBAcl } from 'ncmb-react-native';
import config from '../app.json';

// NCMBの初期化
new NCMB(config.applicationKey, config.clientKey);
```

後は同じ内容で返します。

```js
export default NCMB;
export {
  NCMBObject,
  NCMBQuery,
  NCMBUser,
  NCMBAcl,
};
```

app.jsonは次のような内容です。

```json
{
  "applicationKey": "YOUR_APPLICATION_KEY",
  "clientKey": "YOUR_CLIENT_KEY"
}
```

これでncmb.jsを読み込めば、初期化済みのReact Native SDKを利用できます。

## Todoの新規入力について

Todoの入力はInput.jsにて行います。まず、入力テキスト用のStateを用意します。

```js
// 実装済み
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { TextInput } from 'react-native-paper';
import { NCMBObject } from './ncmb';

export default function InputTodo(props) {
  // 入力テキスト用
  const [text, setText] = React.useState('');
}
```

そして、テキストボックスでエンターキーを押したタイミング（IME確定は除く）で、データを保存します。利用するのは NCMBObject になります。NCMBObjectはDBでいう行相当のデータになります。 `set` メソッドでデータを適用し（カラムに対する値の設定に相当します）、saveメソッドで保存します。

```js
// 1. 実装してください
// エンターキーを押した際にデータストアに保存する
const keyPress = async (e) => {
  if(e.which === 13){
    try {
      // タスクを新規保存します
      const obj = new NCMBObject('Todo');
      await obj
        .set('body', text)
        .set('checked', false)
        .save();
      props.added(obj);
      setText('');
    } catch (e) {
      console.log(e);
    }
  }
};
```

`props.added` はHome.jsから送られてきている、データ追加後に実行する関数です。

```js
// 実装済み
// タスクを追加した後に呼ばれるアクション
const added = (obj) => {
  ary.push(obj);      // 配列にタスクを追加
  setArray([...ary]); // 配列を更新
  // バナーでメッセージ表示
  setMessage('タスクを追加しました');
};
```

## Todoの一覧表示について

Todoの表示はHome.jsにて行います。

```js
// 実装済み
import React, { useState, useEffect } from 'react';
import { Text, View, StyleSheet, FlatList } from 'react-native';
import { List, Button } from 'react-native-paper';
import InputTodo from './Input';
import { NCMBQuery } from './ncmb';
import TodoItem from './TodoItem';
import Banner from './Banner';

export default function Home({ navigation }) {
}
```

まずTodoの配列が入る変数（ary）、バナーでのメッセージ内容を管理する変数（message）を用意します。

```js
// 実装済み
// Todoの配列が入る変数  
const [ary, setArray] = useState([]);
// バナーで表示するメッセージ
const [message, setMessage] = useState('');
```

次に初期表示したタイミングで、データを取得します。この時にはNCMBQueryを使います。引数はクラス名（DBでいうテーブル相当）です。

```js
// 2. 実装してください
// タスクを取得する関数
const fetchTodo = async () => {
  // NCMBのTodoクラス（DBでいうテーブル相当）を準備
  const query = new NCMBQuery("Todo");
  // 取得する件数
  query.limit = 100;
  // データを取得
  const ary = await query.fetchAll();
  // 配列にセット
  setArray(ary);
};

// 最初に一度だけ実行する処理
useEffect(() => {
  fetchTodo();
}, []);
```

後は描画内容として、このaryをFlatListで表示します。

`ShowDelete` は、削除対象のタスクが1つでもあれば、ボタンを有効にします。


```js
// 実装済み
// 削除ボタンの有効、無効を切り替えつつ表示する  
const ShowDelete = () => {
  return (
    <Button
      icon="delete"
      mode="contained"
      onPress={deleteTask}
      // 1件でも完了したタスクがあれば、削除を表示
      disabled={!(ary.filter(item => item.get('checked')).length > 0)}
    >
      <Text>削除</Text>
    </Button>
  )
};
```

`TodoItem` は一行あたりのTodoを表示するコンポーネントです。

## 更新について

データの更新は2種類あります。

- チェックボックスを付けて、Todoデータを終了にする
- Todoの内容を変更する

### チェックボックスを付けて、Todoデータを終了にする

チェックボックスはTodoItem.jsにあります。

CheckboxのonPressにて、checked関数を実行します。

```js
// 実装してください
// タスクをチェックした際に呼ばれるアクション
const checked = ({index, item }) => {
  // データを更新（フラグを反転）
  item.set('checked', !item.get('checked'));
  // 保存
  item.save();
  updated({index, item});
};
```

updated関数は、Home.jsから送られてきている関数で、バナーを使ってタスクの更新を通知します。

```js
// 実装済み
// タスクを更新した後に呼ばれるアクション
const updated = ({ item, index }) => {
  ary[index] = item;  // 配列の更新
  setArray([...ary]); // 配列を更新
  // バナーでメッセージ表示
  setMessage('タスクを更新しました');
};
```

### Todoの内容を変更する

Todoの内容を変更するのは、EditScreenになります。まず一覧の時点でTodoを選択すると、EditScreenに遷移します。List.ItemのonPressイベントになります。

EditScreenでは、まず入力内容を取得します。

```js
// 実装済み
import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import { TextInput } from 'react-native-paper';

export default function EditTodo({ route, navigation }) {
  const {index, item, updated} = route.params;
  const [text, setText] = useState(item.get('body'));
}
```

タスクの更新は、新規登録の際と同じく、入力確定のタイミングで処理を行います。新しいTodoの内容をitem（NCMBObject）に適用して保存します。その後、バナーでの更新通知を行い、 `navigation.goBack()` を使って前の画面（一覧画面）へ戻ります。

```js
// 実装してください
// エンターキーが押された際のイベントです
const keyPress = async (e) => {
  if(e.which === 13){
    try {
      // タスクを更新します
      await item
        .set('body', text)
        .save();
      updated({index, item });
      navigation.goBack();
    } catch (e) {
      console.log(e);   
    }
  }
};
```

## 削除について

Todoの削除はHome.jsにて行います。チェックが付いたタスクを対象に、データを削除します。まず1つでもチェックが付いているタスクがあるかどうかを判定し、ボタンの有効/無効を切り替えます。

```js
// 実装済み
// 削除ボタンの有効、無効を切り替えつつ表示する  
const ShowDelete = () => {
  return (
    <Button
      icon="delete"
      mode="contained"
      onPress={deleteTask}
      // 1件でも完了したタスクがあれば、削除を表示
      disabled={!(ary.filter(item => item.get('checked')).length > 0)}
    >
      <Text>削除</Text>
    </Button>
  )
};
```

そしてボタンを押した際のイベントにて deleteTask 関数を実行します。複数のタスクが対象になるので、Promiseオブジェクトを返します。そしてPromise.allを使って処理の完了を待ちます。

データを削除したら、改めてデータを取得し直しています。もちろんaryから削除するだけでも良いでしょう。

```js
// 実装してください
// 完了しているタスクを削除するイベント
const deleteTask = async () => {
  // チェックの有無を確認して削除処理実行
  const promises = ary.map(item => {
    if (item.get('checked')) {
      return item.delete();
    }
  });
  // Promiseを処理待ち
  await Promise.all(promises);
  // バナーでメッセージ表示
  setMessage('タスクを削除しました');
  // 改めてデータを取得し直す
  fetchTodo();
};
```

これでTodoアプリの完成です。