1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-06-15

初めに

前回の記事

Go + TypeScript構成でチュートリアルWebアプリ作ってみる(2) ~バックエンドにCRUDのAPI作成~

修正後のディレクトリ構造(バックエンドは今回は割愛、不要そうな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設定

バックエンドAPIの直接の呼び出し定義

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

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');
    }
  },
}; 

タスク管理のロジックを集約

frontendo/hooks/useTasks.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
  };
}; 

新規タスクを追加する部品を作成

frontend/components/TaskForm.tsx
コンポーネント化したほうが管理しやすそうなので、分割して実装

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>
  );
}; 

各タスクの表示

frontend/components/TaskItem.tsx
こっちもコンポーネント化

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

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

タスク追加

image.png

image.png

タスク編集

image.png

image.png

タスク削除

CRUDは問題なさそう

image.png

終わりに

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?