【ReactNative】自作ローカルAPIを用いて、TODOアプリを作ってみた

## はじめに

前回、TODOアプリのAPIを作成して、ローカルの挙動確認まで実施しました。
その続きとして、ReactNativeでTODOアプリを作成して、APIを叩く実装をします。

以下は、前回の記事のリンクです。

https://qiita.com/muranakar/items/8b7fbaf9c27e53241ae1

## 内容

まず最初に、前回のAPIの修正が必要になります。

### 追加実装した内容
- `id`をすべてUUIDに変更
- ローカルサーバーAPIを叩く際のポート番号と接続の権限周りの変更



### `id`をすべてUUIDに変更
変更する理由は、`number`で管理していると、数字の重複の可能性があるためです。

uuidライブラリーをインストール
```
 npm install uuid
```

以下のコードに修正。
<details><summary>App.jsの変更後のコード</summary>

```js
const express = require('express');
const { v4: uuidv4 } = require('uuid'); // uuidライブラリからv4メソッドをインポート
const app = express();
const port = 3000;

// CORS設定
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8081'); // 許可するオリジン
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});

// To-Doリストの仮データ
const todos = [
  { id: uuidv4(), title: 'タスク1', completed: false },
  { id: uuidv4(), title: 'タスク2', completed: true },
];

// JSONデータのパースを有効にする
app.use(express.json());

// To-Doリストの取得
app.get('/todos', (req, res) => {
  res.json(todos);
});

// To-Doリストの作成
app.post('/todos', (req, res) => {
  const newTodo = req.body;
  newTodo.id = uuidv4(); // 新しいToDoのidにUUIDを設定
  todos.push(newTodo);
  res.status(201).json(newTodo);
});

// To-Doリストの更新
app.put('/todos/:id', (req, res) => {
  const todoId = req.params.id; // UUIDをそのまま使用
  const updatedTodo = req.body;
  const todoIndex = todos.findIndex(todo => todo.id === todoId);
  if (todoIndex !== -1) {
    todos[todoIndex] = { ...todos[todoIndex], ...updatedTodo };
    res.json(todos[todoIndex]);
  } else {
    res.status(404).json({ message: '指定されたToDoが見つかりません' });
  }
});

// To-Doリストの削除
app.delete('/todos/:id', (req, res) => {
  const todoId = req.params.id; // UUIDをそのまま使用
  const todoIndex = todos.findIndex(todo => todo.id === todoId);
  if (todoIndex !== -1) {
    const deletedTodo = todos.splice(todoIndex, 1)[0];
    res.json(deletedTodo);
  } else {
    res.status(404).json({ message: '指定されたToDoが見つかりません' });
  }
});

// サーバーを起動
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});


```
</details>

### ローカルサーバーAPIを叩く際のポート番号と接続の権限周りの変更

CORSエラーより、以下のコードを追加しました。
```js
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8081'); // 許可するオリジン
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  next();
});
```
詳細の説明は以下のリンクが参考になりました。

https://zenn.dev/luvmini511/articles/d8b2322e95ff40

## API通信をおこなうためのモジュールを作成

### axios（API通信を行うため）をインストール
```
npm install axios
```

### todoapi.jsの作成
APIを叩くためのクラスを作成すために、todoapi.jsファイルを作成します。

```js
import axios from 'axios';

const API_URL = 'http://localhost:3000';

// TODOリストを取得する関数
export const fetchTodos = async () => {
  try {
    const response = await axios.get(`${API_URL}/todos`);
    return response.data;
  } catch (error) {
    throw error;
  }
}

// 新しいTODOを作成する関数
export const createTodo = async (newTodoData) => {
  try {
    const response = await axios.post(`${API_URL}/todos`, newTodoData);
    return response.data;
  } catch (error) {
    throw error;
  }
}

// TODOを更新する関数
export const updateTodo = async (todoId, updatedTodoData) => {
  try {
    const response = await axios.put(`${API_URL}/todos/${todoId}`, updatedTodoData);
    return response.data;
  } catch (error) {
    throw error;
  }
}

// TODOを削除する関数
export const deleteTodo = async (todoId) => {
  try {
    const response = await axios.delete(`${API_URL}/todos/${todoId}`);
    return response.data;
  } catch (error) {
    throw error;
  }
}
```
- 上記の関数は全て非同期関数（`async` `await`）で定義されており、TODOリストの`取得GET`　`追加POST`　`更新PUT`　`削除DELETE`　を実装しています
- `try` と `catch` は、JavaScriptにおいてエラーハンドリングに使用される構文です。API通信が失敗した際にエラーを取得し、`throw`で投げることができます
- Axiosライブラリを使用して、指定されたAPIのURLからToDoリストのデータを非同期通信でCRUD処理を行っています

GET
```js
const response = await axios.get(${API_URL}/todos);
```
POST
```js
const response = await axios.post(`${API_URL}/todos`, newTodoData);
```

PUT
```js
const response = await axios.put(`${API_URL}/todos/${todoId}`, updatedTodoData);
```

DELETE
```js
 const response = await axios.delete(`${API_URL}/todos/${todoId}`);
```

## TODOアプリのメイン画面の作成

App.jsの作成し、実装していきます。

### 変数を定義
TODOリストと新規作成したTODOを格納するための変数を定義します。

```js
  const [todos, setTodos] = useState([]);
  const [newTodoText, setNewTodoText] = useState('');
```

### 登録されているTODOリストの値を取得

```js
 useEffect(() => {
    fetchTodos()
      .then((data) => setTodos(data))
      .catch((error) => console.error('TODOリストの取得に失敗しました', error));
  }, []);
```

`useEffect`はReactのhooksの一つであり、でTODOリストの初期値を取得するために実装しています。この`useEffect`は空のリストを持っているため、コンポーネントが初めてレンダリングされた直後に1回だけ呼び出されます。


### ボタンが押されたときのCRUD処理を定義
```js
const handleCreateTodo = () => {
    if (newTodoText.trim() === '') {
      return;
    }
    const newTodoData = {
      title: newTodoText,
      completed: false,
    };
    
    createTodo(newTodoData)
      .then((createdTodo) => {
        setTodos((prevTodos) => [...prevTodos, createdTodo]);
        setNewTodoText('');
      })
      .catch((error) => console.error('TODOの作成に失敗しました', error));
  };
```
- 新しく追加するTODOである`newTodoText` の値が空白でないことを確認しています。もし空白の場合は、何もせずに関数から抜けます
- 新しく作成されたTODOが `createTodo` 関数によって返されたら、そのTODOを `setTodos` を使用して現在のTODOリストに追加します
- `setNewTodoText('')` を呼び出して、新しいTODOのテキスト入力フィールドをクリアします
- `createTodo` 関数でエラーが発生した場合、エラーをコンソールに出力します


同様に、読み込み・更新・削除を実装していく。その辺りに関しては、サンプルコードのみ添付いたします。

<details><summary>サンプルコード</summary>

```js
  const handleUpdateTodo = (todoId, updatedData) => {
    // TODOを更新し、stateを更新
    updateTodo(todoId, updatedData)
      .then((updatedTodo) => {
        setTodos((prevTodos) =>
          prevTodos.map((todo) => (todo.id === updatedTodo.id ? updatedTodo : todo))
        );
      })
      .catch((error) => console.error('TODOの更新に失敗しました', error));
  };

  const handleDeleteTodo = (todoId) => {
    // TODOを削除し、stateから削除
    deleteTodo(todoId)
      .then((deletedTodo) => {
        setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== deletedTodo.id));
      })
      .catch((error) => console.error('TODOの削除に失敗しました', error));
  };

  const handleReloadTodo = () => {
    fetchTodos()
      .then((data) => setTodos(data))
      .catch((error) => console.error('TODOリストの取得に失敗しました', error));
  };

```
</details>

### 追加・更新ボタンの追加

```js
  return (
    <View>
      <Button title="更新" onPress={handleReloadTodo} />
      <Text>TODOリスト</Text>
      <TextInput
        placeholder="新しいTODOを入力"
        value={newTodoText}
        onChangeText={(text) => setNewTodoText(text)}
      />
      <Button title="追加" onPress={handleCreateTodo} />
    {/* 省略 */}
    </View>
  );
```

- `Button`コンポーネントが2つあります。一つは更新ボタンで、`handleReloadTodo`関数が押されたときに呼び出されます。もう一つは追加ボタンで、`handleCreateTodo`関数が押されたときに呼び出されます

- `TextInput`コンポーネントは新しいTODOを入力するためのテキストフィールドです。`value`プロパティは`newTodoText`とバインドされており、ユーザーが入力したテキストは`setNewTodoText`関数を介して更新されます

### TODOのリスト表示
```js
    <FlatList
        data={todos}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <View>
            <TouchableOpacity onPress={() => handleUpdateTodo(item.id, { completed: !item.completed })}>
              <Text style={{ textDecorationLine: item.completed ? 'line-through' : 'none' }}>
                {item.title}
              </Text>
            </TouchableOpacity>
            <Button title="削除" onPress={() => handleDeleteTodo(item.id)} />
          </View>
        )}
      />
```



- `FlatList`コンポーネントはTODOリストの表示を行います。`data`プロパティには`todos`配列が渡されており、`keyExtractor`プロパティは各アイテムの一意のIDを取得するための関数を指定しています

- `renderItem`プロパティは各TODOアイテムの表示方法を定義します。各アイテムは`TouchableOpacity`でラップされており、タップされたときに`handleUpdateTodo`関数が呼び出され、TODOの完了状態が切り替わります。TODOが完了している場合はテキストに取り消し線が追加されます。また、削除ボタンがあり、それを押すと`handleDeleteTodo`関数が呼び出され、対応するTODOアイテムが削除されます

## 挙動確認
TODOのAPIをローカル環境で立ち上げ、シミュレーターで確認

<img width="600" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/799713/3bc16acf-d1be-a6b0-da13-ec5edec4880d.gif">

API通信を用いて、Webブラウザ・シミュレーター上でTODO管理を行うことができました。



解説は以上になります。
サンプルコード添付しておきます。

<details><summary>App.js</summary>

```js
import React, { useEffect, useState } from 'react';
import { View, Text, Button, TextInput, FlatList, TouchableOpacity , StyleSheet } from 'react-native';
import { fetchTodos, createTodo, updateTodo, deleteTodo } from './api/todoapi';
import 'react-native-get-random-values';


function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [newTodoText, setNewTodoText] = useState('');

  useEffect(() => {
    fetchTodos()
      .then((data) => setTodos(data))
      .catch((error) => console.error('TODOリストの取得に失敗しました', error));
  }, []);

  const handleCreateTodo = () => {
    if (newTodoText.trim() === '') {
      return;
    }
    const newTodoData = {
      title: newTodoText,
      completed: false,
    };
    createTodo(newTodoData)
      .then((createdTodo) => {
        setTodos((prevTodos) => [...prevTodos, createdTodo]);
        setNewTodoText('');
      })
      .catch((error) => console.error('TODOの作成に失敗しました', error));
  };

  const handleUpdateTodo = (todoId, updatedData) => {
    updateTodo(todoId, updatedData)
      .then((updatedTodo) => {
        setTodos((prevTodos) =>
          prevTodos.map((todo) => (todo.id === updatedTodo.id ? updatedTodo : todo))
        );
      })
      .catch((error) => console.error('TODOの更新に失敗しました', error));
  };

  const handleDeleteTodo = (todoId) => {
    deleteTodo(todoId)
      .then((deletedTodo) => {
        setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== deletedTodo.id));
      })
      .catch((error) => console.error('TODOの削除に失敗しました', error));
  };

  const handleReloadTodo = () => {
    fetchTodos()
      .then((data) => setTodos(data))
      .catch((error) => console.error('TODOリストの取得に失敗しました', error));
  };

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

    
      <Button title="更新" onPress={handleReloadTodo} />
      <Text style={styles.header}>TODOリスト</Text>
      <TextInput
        placeholder="新しいTODOを入力"
        value={newTodoText}
        onChangeText={(text) => setNewTodoText(text)}
        style={styles.input}
      />
      <Button title="追加" onPress={handleCreateTodo} />
      <FlatList
        data={todos}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <View style={styles.todoItem}>
            <TouchableOpacity onPress={() => handleUpdateTodo(item.id, { completed: !item.completed })}>
              <Text style={[styles.todoText, item.completed ? styles.completedTodo : null]}>
                {item.title}
              </Text>
            </TouchableOpacity>
            <Button title="削除" onPress={() => handleDeleteTodo(item.id)} />
          </View>
        )}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#fff',
    marginTop: 100
  },
  header: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  input: {
    borderWidth: 1,
    borderColor: 'gray',
    borderRadius: 4,
    padding: 8,
    marginBottom: 16,
  },
  todoItem: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 8,
  },
  todoText: {
    fontSize: 18,
  },
  completedTodo: {
    textDecorationLine: 'line-through',
    color: 'gray',
  },
});

export default TodoApp;

```

</details>

<details><summary>todoapi.js</summary>

```js
import axios from 'axios';

const API_URL = 'http://localhost:3000';

// TODOリストを取得する関数
export const fetchTodos = async () => {
  try {
    const response = await axios.get(`${API_URL}/todos`);
    return response.data;
  } catch (error) {
    throw error;
  }
}

// 新しいTODOを作成する関数
export const createTodo = async (newTodoData) => {
  try {
    const response = await axios.post(`${API_URL}/todos`, newTodoData);
    return response.data;
  } catch (error) {
    throw error;
  }
}

// TODOを更新する関数
export const updateTodo = async (todoId, updatedTodoData) => {
  try {
    const response = await axios.put(`${API_URL}/todos/${todoId}`, updatedTodoData);
    return response.data;
  } catch (error) {
    throw error;
  }
}

// TODOを削除する関数
export const deleteTodo = async (todoId) => {
  try {
    const response = await axios.delete(`${API_URL}/todos/${todoId}`);
    return response.data;
  } catch (error) {
    throw error;
  }
}


```
</details>


## 最後に
他にも間違えている点、他に良い方法などありましたら、コメントいただけますと大変うれしいです。
良かったと思ったら、いいねやTwitterのフォローよろしくお願いいたします！

https://sites.google.com/view/muranakar

個人でアプリを作成しているので、良かったら覗いてみてください