GraphQL、Go、React、TypeScriptを使ったTodoアプリの開発（フロントエンド編）

本記事は HRBrain Advent Calendar 2023 の8日目の記事です。

https://qiita.com/advent-calendar/2023/hrbrain

# はじめに

GraphQLをハンズオンで学ぶためにTodoアプリを作ってみました。
本記事ではpart2として、GraphQLクエリを使ったTodoアプリのフロントエンド開発手順をまとめました。

part1のバックエンド編はこちらをご覧ください。

https://qiita.com/tomotaka_s/items/7a51ddab5294ede9fc11

### 使用した技術スタック

言語：Go、TypeScript
ライブラリ：React、gqlgen、codegen、xorm
DB：Postgres

gqlgenとcodegenはGraphQLのスキーマ定義から、それぞれGoとTypeScriptのコードを自動生成してくれるライブラリです。
xormはgoのORMライブラリで、DBにはPostgresを使用しました。

# Todoアプリの概要

Todoアプリの完成イメージはこちらです。本記事ではこのフロントエンド部分についてハンズオン形式でまとめます。
※Chromeのプラグインで[GraphQL Network Inspector](https://chrome.google.com/webstore/detail/graphql-network-inspector/ndlbedplllcgconngcnfmkadhokfaaln)を使っています。このプラグインでは、GraphQLのリクエスト・レスポンスのみを見ることができます。
![todoapp.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/588214/6c75a5b9-ae01-0cc6-4ab1-ddcd5816c6de.gif)

また主にGraphQLに関連する部分を解説するため、適宜ソースコードを参照してください〜！🙏

https://github.com/tomotakashimizu/todoapp-graphql-go-react

### ディレクトリ構造

ディレクトリ構造は下記のようになっており、本記事ではfront（フロントエンドのアプリケーションコード）を作成していきます。

```
.
├── app                                            - バックエンドのアプリケーションコード
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   ├── gqlgen.yml                                 - gqlgenの設定ファイル
│   ├── graph                                      - GraphQL関連のコード
│   │   ├── generated.go                           - gqlgenによって自動生成されるGoのコード
│   │   ├── model                                  - GraphQLモデルを定義するGoのコード
│   │   │   ├── models_gen.go                      - gqlgenによって自動生成されるモデルのコード
│   │   │   └── todo.go
│   │   ├── resolver.go                            - GraphQLリゾルバのインターフェースを定義する
│   │   └── schema.resolvers.go                    - リゾルバインターフェースを具体的に実装するコード
│   ├── infrastructure
│   │   └── todo.go                                - インフラのTodoモデル
│   ├── server.go                                  - サーバーコードのエントリーポイント
│   ├── sql
│   │   └── init.sql                               - 初期化SQLスクリプト
│   └── tools.go                                   - gqlgenをインストール用のファイル
│
├── front                                          - フロントエンドのアプリケーションコード
│   ├── Dockerfile
│   ├── codegen.yml                                - codegen(フロントのGraphQLコード生成)の設定ファイル
│   ├── graphql                                    - GraphQLのクエリやスキーマを定義
│   │   ├── mutation                               - データを変更するGraphQLのmutation定義
│   │   │   ├── createTodo.graphql
│   │   │   ├── deleteTodo.graphql
│   │   │   └── updateTodoStatus.graphql
│   │   └── query                                  - データを取得するGraphQLのquery定義
│   │       └── getAllTodos.graphql
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── src                                        - アプリケーションのソースコード
│   │   ├── App.tsx                                - メインのアプリケーションコンポーネント
│   │   ├── components                             - 再利用可能なUIコンポーネント
│   │   │   ├── CreateTodoForm.tsx
│   │   │   └── TodoList.tsx
│   │   ├── index.tsx                              - アプリケーションのエントリーポイント
│   │   └── types                                  - TypeScriptの型定義
│   │       └── gen                                - codegenによって自動生成される型定義
│   │           ├── api.ts                         - APIの型定義
│   │           └── possibleTypes-result.ts
│   └── tsconfig.json
│
├── docker-compose.yml
└── schema.graphqls                                - 共通のGraphQLスキーマを定義
```

# Todoアプリのハンズオン
では、実際にGraphQLを使ったTodoアプリのフロントエンド開発手順を解説します。

## フロントエンド開発手順の概要

1. プロジェクトのセットアップ
2. GraphQL Code Generator の設定
3. GraphQL QueryとMutationの定義
4. graphql-codegenでコード自動生成
5. Apollo Client の設定
6. コンポーネントの実装

### 1. プロジェクトのセットアップ

まず、新しいディレクトリを作成し、React アプリケーションの雛形を作成します。`create-react-app` を使用して TypeScript テンプレートを利用してプロジェクトを始めることができます。

```zsh
cd ~/go/src/github.com/[username]/todoapp-graphql-go-react
mkdir front
cd front
npx create-react-app front --template typescript
```

次に、GraphQL に必要なパッケージをインストールします。

```zsh
npm install graphql
npm install --save-dev @graphql-codegen/cli @graphql-codegen/schema-ast @graphql-codegen/fragment-matcher @graphql-codegen/typescript @graphql-codegen/typescript-operations @apollo/client @graphql-codegen/typescript-react-apollo
```

### 2. GraphQL Code Generator の設定

次に、codegen.yml ファイルを作成して、GraphQL Code Generatorの設定を行います。このツールは、GraphQLクエリとスキーマに基づいてTypeScriptの型定義やReactのHooksを自動的に生成するツールです。

```yml:front/codegen.yml
# GraphQLスキーマとドキュメントのソースを定義
schema:
  - ../*.graphqls

documents:
  - ./graphql/mutation/*.graphql
  - ./graphql/query/*.graphql

# 生成される出力とプラグインの設定
generates:
  # スキーマをAST形式で生成
  graphql/schema.graphql:
    plugins:
      - schema-ast

  # フラグメントマッチャーの生成
  src/types/gen/possibleTypes-result.ts:
    plugins:
      - fragment-matcher
    config:
      apolloClientVersion: 3

  # TypeScriptの型、操作、およびReact hooksの生成
  src/types/gen/api.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withComponent: false
      withHooks: true

# グローバル設定
config:
  gqlImport: '@apollo/client#gql'
  scalars:
    Date: string
    NumberID: number
    Time: string
    Version: number
```

この設定ファイルにより、GraphQLスキーマを基にして必要な型やコンポーネントの骨格が生成されます。

### 3. GraphQL Query と Mutation の定義

GraphQLクエリ（データの取得）とミューテーション（データの更新）を定義します。これらは、サーバーにどのようなデータを要求するか、またはどのような操作を行うかを指定するための重要な部分です。

#### Queryの例

```graphql:front/graphql/query/getAllTodos.graphql
# GetAllTodosクエリは、サーバーから全てのTodo項目を取得するために使用されます。
query GetAllTodos {
  todos {
    id    # 各Todo項目のユニークなID
    text  # Todo項目のテキスト内容
    done  # Todoが完了したかどうかの状態
  }
}
```

#### Mutationの例

```graphql:front/graphql/mutation/createTodo.graphql
# CreateTodoミューテーションは、新しいTodo項目を作成するために使用されます。
mutation CreateTodo($todoInput: CreateTodoInput!) {
  createTodo(todoInput: $todoInput) {
    id    # 作成されたTodo項目のID
    text  # 作成されたTodo項目のテキスト
    done  # 作成されたTodoの状態（デフォルトでは未完了）
  }
}
```

これらのクエリとミューテーションは、アプリケーションがサーバーとの間でどのようにデータを交換するかを定義します。

### 4. graphql-codegenでコード自動生成

次に、GraphQL Code Generatorを使用して、上記で定義したクエリとミューテーションに基づいてコードを自動生成します。

```zsh
npm run graphql-codegen --config codegen.yml
```

このコマンドにより、API操作に必要な型やフックが自動的に生成され、開発プロセスを大幅に加速します。

### 5. Apollo Client の設定

Apollo Clientは、ReactアプリケーションとGraphQLサーバー間の通信を管理するためのライブラリです。ここでは、Apollo Clientを設定し、GraphQLサーバーへの接続を確立します。

```tsx:front/src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

// Apollo Clientの設定
const client = new ApolloClient({
  uri: 'http://localhost:8081/query',  // GraphQLサーバーのエンドポイント
  cache: new InMemoryCache(),          // クライアントサイドのキャッシュ管理
});

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <React.StrictMode>
    <ApolloProvider client={client}>   {/* Apollo ClientをReactアプリケーション全体で利用可能にする */}
      <App />
    </ApolloProvider>
  </React.StrictMode>
);
```

この設定により、アプリケーションはGraphQLサーバーにクエリを送信し、データを取得できるようになります。

### 6. コンポーネントの実装

最後に、Todoアプリの主要なUIコンポーネントを実装します。以下に、Todoリストを表示し、新しいTodoを追加するフォームの実装例を示します。

#### Todoリストコンポーネントのコード

```tsx:front/src/components/TodoList.tsx
import React from 'react';
import { DeleteTodoDocument, useGetAllTodosQuery, UpdateTodoStatusDocument } from '../types/gen/api';
import { useMutation } from '@apollo/client';

export const TodoList: React.FC = () => {
  const { data, loading, error, refetch } = useGetAllTodosQuery();  // サーバーからTodoリストを取得
  const [updateTodoStatus] = useMutation(UpdateTodoStatusDocument); // Todoステータスを更新するミューテーション
  const [deleteTodo] = useMutation(DeleteTodoDocument);             // Todoを削除するミューテーション

  // Todoのステータスを更新する関数
  const handleUpdateTodoStatus = async (todoId: string, done: boolean) => {
    try {
      await updateTodoStatus({ variables: { todoId, done } });
      refetch();
    } catch (error) {
      console.error('Error updating todo status: ', error);
    }
  };

  // Todoを削除する関数
  const handleDeleteTodo = async (todoId: string) => {
    try {
      await deleteTodo({ variables: { todoId } });
      refetch();
    } catch (error) {
      console.error('Error deleting todo: ', error);
    }
  };

  // データの読み込み中、またはエラーがある場合の処理
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  // TodoリストのUIレンダリング
  return (
    <div>
      <h2>Todoリスト</h2>
      {/* 未完了のTodo項目をリストアップ */}
      <h3>未完了のTODO</h3>
      <ul>
        {data?.todos.map(
          (todo) =>
            todo.done || ( /* 未完了のTodoのみ表示 */
              <li key={todo.id}>
                {todo.text}
                <button onClick={() => handleUpdateTodoStatus(todo.id, true)}>完了</button>
                <button onClick={() => handleDeleteTodo(todo.id)}>削除</button>
              </li>
            )
        )}
      </ul>

     {/* 完了したTodo項目をリストアップ */}
      <h3>完了したTODO</h3>
      <ul>
        {data?.todos.map(
          (todo) =>
            todo.done && ( /* 完了したTodoのみ表示 */
              <li key={todo.id}>
                {todo.text}
                <button onClick={() => handleUpdateTodoStatus(todo.id, false)}>未完了に戻す</button>
                <button onClick={() => handleDeleteTodo(todo.id)}>削除</button>
              </li>
            )
        )}
      </ul>
    </div>
  );
};
```

#### Todo作成フォームのコード

```tsx:front/src/components/CreateTodoForm.tsx
import React, { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CreateTodoDocument, GetAllTodosDocument, GetAllTodosQuery } from '../types/gen/api';

function CreateTodoForm() {
  const [text, setText] = useState(''); // 新しいTodoのテキストを保持するstate
  const [createTodo] = useMutation(CreateTodoDocument, {
    // 新しいTodoを追加した後、キャッシュを更新してUIを自動的に反映させる
    update(cache, { data: { createTodo } }) {
      const existingTodos = cache.readQuery<GetAllTodosQuery>({ query: GetAllTodosDocument });

      if (existingTodos && createTodo) {
        cache.writeQuery({
          query: GetAllTodosDocument,
          data: { todos: [...existingTodos.todos, createTodo] },
        });
      }
    },
  });

  // フォームのサブミット処理
  const handleSubmit = async (event: { preventDefault: () => void }) => {
    event.preventDefault();

    if (!text.trim()) return;

    try {
      const todoInput = { text };
      await createTodo({ variables: { todoInput } });

      setText('');
    } catch (error) {
      console.error('Error adding todo: ', error);
    }
  };

  // Todo作成フォームのUIレンダリング
  return (
    <form onSubmit={handleSubmit}>
      <input type='text' value={text} onChange={(event) => setText(event.target.value)} placeholder='Add new todo' />
      <button type='submit'>Add</button>
    </form>
  );
}

export default CreateTodoForm;
```

ここまでが、GraphQLを使ってTodoアプリのフロントエンドを開発する基本的なステップです。必要に応じて各ステップの詳細な説明や、それぞれの技術の基本的な知識などは、各技術の公式ドキュメントを参照してください！👍

# おわりに

簡単なTodoアプリ開発ですが、GraphQLの基本的な部分をハンズオンで学べてよかったです！
まだ学べていない範囲もあるので、これからも勉強していきます。
ここまで読んでいただきありがとうございました！

HRBrain では一緒に働いてくれる仲間を募集しています。興味がありましたら、ぜひご応募ください。

https://www.hrbrain.co.jp/recruit

# 参考リンク

https://graphql.org/

https://the-guild.dev/graphql/codegen