Go + TypeScript構成でチュートリアルWebアプリ作ってみる(3) ~フロントエンドからAPIを呼び出す~

## 初めに
### 前回の記事
[Go + TypeScript構成でチュートリアルWebアプリ作ってみる(2) ~バックエンドにCRUDのAPI作成~](https://qiita.com/minorin_/items/e679867847db1eff0b9e)

### 修正後のディレクトリ構造(バックエンドは今回は割愛、不要そうなmodule系は省いています)
実装順に記載していきます。
```
frontend/
├── src/
│   ├── api/
│   │   └── taskApi.ts // バックエンドとの通信
│   │
│   ├── hooks/
│   │   └── useTasks.ts // タスク管理のロジック
│   │
│   ├── components/
│   │   ├── TaskForm.tsx // 新規タスク入力コンポーネント
│   │   └── TaskItem.tsx // 個々のタスク表示コンポーネント
│   │
│   ├── App.tsx // メインコンポーネント
│   ├── App.module.css // css
│   ├── index.css // css
│   └── main.tsx // エントリーポイント
│   
├── package.json // 依存関係の定義
└── tsconfig.json // TypeScript設定
```

<br>

## バックエンドAPIの直接の呼び出し定義
frontend/src/api/taskApi.ts
バックエンドで実装したCRUDのAPIをフロントで呼び出せるように定義しています

```ts
export interface Task {
  id: number;
  title: string;
}

const BACKEND_BASE_URL = 'http://localhost:8080';

export const taskApi = {
  // タスク一覧の取得
  getTasks: async (): Promise<Task[]> => {
    const response = await fetch(`${BACKEND_BASE_URL}/api/todos`);
    if (!response.ok) {
      throw new Error('Failed to fetch tasks');
    }
    return response.json();
  },

  // タスクの作成
  createTask: async (title: string): Promise<Task> => {
    const response = await fetch(`${BACKEND_BASE_URL}/api/todos`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ title }),
    });
    if (!response.ok) {
      throw new Error('Failed to create task');
    }
    return response.json();
  },

  // タスクの更新
  updateTask: async (id: number, title: string): Promise<Task> => {
    const response = await fetch(`${BACKEND_BASE_URL}/api/todos`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ id, title }),
    });
    if (!response.ok) {
      throw new Error('Failed to update task');
    }
    return response.json();
  },

  // タスクの削除
  deleteTask: async (id: number): Promise<void> => {
    const response = await fetch(`${BACKEND_BASE_URL}/api/todos?id=${id}`, {
      method: 'DELETE',
    });
    if (!response.ok) {
      throw new Error('Failed to delete task');
    }
  },
}; 
```

<br>

## タスク管理のロジックを集約
frontendo/hooks/useTasks.ts
状態を変更するロジックをまとめておく

<details><summary>コード詳細</summary>

```ts
import { useState, useEffect } from 'react';
import { Task, taskApi } from '../api/taskApi';

export const useTasks = () => {
  // 状態管理
  const [tasks, setTasks] = useState<Task[]>([]);
  const [newTaskTitle, setNewTaskTitle] = useState('');
  const [editingTask, setEditingTask] = useState<Task | null>(null);
  const [editTitle, setEditTitle] = useState('');

  // タスク一覧を取得する関数
  const fetchTasks = async () => {
    try {
      const data = await taskApi.getTasks();
      setTasks(data);
    } catch (error) {
      console.error('Error fetching tasks:', error);
    }
  };

  // 新規タスクを追加する関数
  const addTask = async (title: string) => {
    if (!title.trim()) return;
    try {
      await taskApi.createTask(title);
      fetchTasks();
      setNewTaskTitle('');
    } catch (error) {
      console.error('Error creating task:', error);
    }
  };

  // タスクの編集を開始する関数
  const startEditing = (task: Task) => {
    setEditingTask(task);
    setEditTitle(task.title);
  };

  // タスクの編集をキャンセルする関数
  const cancelEditing = () => {
    setEditingTask(null);
    setEditTitle('');
  };

  // タスクの編集を保存する関数
  const saveEdit = async () => {
    if (editingTask && editTitle.trim()) {
      try {
        await taskApi.updateTask(editingTask.id, editTitle);
        fetchTasks();
        setEditingTask(null);
        setEditTitle('');
      } catch (error) {
        console.error('Error updating task:', error);
      }
    }
  };

  // タスクを削除する関数
  const deleteTask = async (id: number) => {
    try {
      await taskApi.deleteTask(id);
      fetchTasks();
    } catch (error) {
      console.error('Error deleting task:', error);
    }
  };

  // 初回レンダリング時にタスク一覧を取得
  useEffect(() => {
    fetchTasks();
  }, []);

  return {
    tasks,
    newTaskTitle,
    editingTask,
    editTitle,
    setNewTaskTitle,
    setEditTitle,
    addTask,
    startEditing,
    cancelEditing,
    saveEdit,
    deleteTask
  };
}; 
```
</details>

<br>

## 新規タスクを追加する部品を作成
frontend/components/TaskForm.tsx
コンポーネント化したほうが管理しやすそうなので、分割して実装

```ts
import React from 'react';
import styles from '../App.module.css';

// propsの型定義
interface TaskFormProps {
  newTaskTitle: string;
  setNewTaskTitle: (title: string) => void;
  addTask: (title: string) => void;
}

// コンポーネントの定義
export const TaskForm: React.FC<TaskFormProps> = ({
  newTaskTitle,
  setNewTaskTitle,
  addTask
}) => {
  // フォーム送信時の処理
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    addTask(newTaskTitle);
  };

  // コンポーネントレンダリング
  return (
    <div className={styles.inputContainer}>
      <form onSubmit={handleSubmit} className={styles.inputGroup}>
        <input
          type="text"
          value={newTaskTitle}
          onChange={(e) => setNewTaskTitle(e.target.value)}
          placeholder="新しいタスクを入力"
          className={styles.input}
        />
        <button type="submit" className={styles.buttonPrimary}>
          追加
        </button>
      </form>
    </div>
  );
}; 
```

<br>

## 各タスクの表示
frontend/components/TaskItem.tsx
こっちもコンポーネント化

```ts
import React from 'react';
import { Task } from '../api/taskApi';
import styles from '../App.module.css';

// propsの型定義
interface TaskItemProps {
  task: Task;
  editingTask: Task | null;
  editTitle: string;
  setEditTitle: (title: string) => void;
  startEditing: (task: Task) => void;
  cancelEditing: () => void;
  saveEdit: () => void;
  deleteTask: (id: number) => void;
}

// コンポーネントの定義
export const TaskItem: React.FC<TaskItemProps> = ({
  task,
  editingTask,
  editTitle,
  setEditTitle,
  startEditing,
  cancelEditing,
  saveEdit,
  deleteTask
}) => {
  // 編集中かどうか
  const isEditing = editingTask?.id === task.id;

  // コンポーネントレンダリング
  return (
    <li className={styles.taskItem}>
      {isEditing ? (
        <div className={styles.inputGroup}>
          <input
            type="text"
            value={editTitle}
            onChange={(e) => setEditTitle(e.target.value)}
            className={styles.input}
          />
          <div className={styles.buttonGroup}>
            <button onClick={saveEdit} className={styles.buttonSuccess}>
              保存
            </button>
            <button onClick={cancelEditing} className={styles.buttonGray}>
              キャンセル
            </button>
          </div>
        </div>
      ) : (
        <div className={styles.taskContent}>
          <span className={styles.taskTitle}>{task.title}</span>
          <div className={styles.buttonGroup}>
            <button onClick={() => startEditing(task)} className={styles.buttonPrimary}>
              編集
            </button>
            <button onClick={() => deleteTask(task.id)} className={styles.buttonDanger}>
              削除
            </button>
          </div>
        </div>
      )}
    </li>
  );
}; 
```

## コンポーネントを組み合わせ
frontend/App.tsx

```ts
import React from 'react';
import { useTasks } from './hooks/useTasks';
import { TaskForm } from './components/TaskForm';
import { TaskItem } from './components/TaskItem';
import styles from './App.module.css';

// メインコンポーネント定義
function App() {
  const {
    tasks,
    newTaskTitle,
    editingTask,
    editTitle,
    setNewTaskTitle,
    setEditTitle,
    addTask,
    startEditing,
    cancelEditing,
    saveEdit,
    deleteTask
  } = useTasks();

  // コンポーネントレンダリング
  return (
    <div className={styles.container}>
      <div className={styles.content}>
        <h1 className={styles.title}>タスク管理</h1>
        <TaskForm
          newTaskTitle={newTaskTitle}
          setNewTaskTitle={setNewTaskTitle}
          addTask={addTask}
        />
        <ul className={styles.taskList}>
          {tasks.map((task) => (
            <TaskItem
              key={task.id}
              task={task}
              editingTask={editingTask}
              editTitle={editTitle}
              setEditTitle={setEditTitle}
              startEditing={startEditing}
              cancelEditing={cancelEditing}
              saveEdit={saveEdit}
              deleteTask={deleteTask}
            />
          ))}
        </ul>
      </div>
    </div>
  );
}

export default App;
```

## 実際に使ってみる
### 初期表示
いい感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/6dc6a9a0-2689-4f91-b98f-a319a4067b42.png)


### タスク追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/846e9899-88ca-470e-991f-4c3639712004.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/5f544eaf-3685-4ce7-890a-968e9dc98569.png)

### タスク編集
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/4c19eec9-7b80-4d54-9e2b-f374d028ddfe.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/527b71e7-8a7d-4d2c-84c3-67e7331817f7.png)

### タスク削除
CRUDは問題なさそう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942091/52cb61e1-cd5e-48dd-8d2e-759e2318e706.png)

## 終わりに
なんとか標準的な開発はできたような気がします
作ってみた感想としてはGoのほうはシンプルに書けるのにTypescriptはだいぶ冗長な書き方するんだなぁという印象です。

いろいろなところからコードかき集めただけのチュートリアルアプリをつくっただけなのに割と時間がかかったので、今後は各箇所で具体的に何をしているのか理解を深めてみたいと思います。