【図解解説】これ1本でGraphQLをマスターできるチュートリアル【React/TypeScript/Prisma】

![graphql_beginner_full_course.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/f3e82821-baf2-ae68-20f9-22e50d520734.png)

こんにちは、Watanabe Jin([@Sicut_study](https://x.com/Sicut_study))です。
今回はGraphQLの初心者向けチュートリアルを行っていきます。

REST APIと肩を並べて知られるGraphQLですが、なかなかイメージがしづらく初心者には難しいものです。私も駆け出しの頃に勉強しましたが全然身につけることができませんでした。
　

- GraphQLのクエリのイメージがしづらい
- DBとGraphQLをどう接続すればいいかわからない
- 実際にアプリケーションと組み合わせる方法がわからない
　

このように教材をやりながら感じていました。
GraphQLはふわっとしてしまう分なるべく実践的に学びたいのですが、実践的な教材は少ないです。
　

これらの欠点を克服するために、チュートリアルでは以下の点を意識して作成しています。
　

- 初心者でもGraphQLをイメージできるように図解を多用してとことん解説しています
- アプリケーションに組み込めるようにReactで実際にアプリを作成して接続します
- GraphQLとPrismaを使って実際のデータベース接続をして本番でも利用できるようにしました
　

チュートリアルを最後までやることで、
**GraphQLを理解できることはもちろんのこと、**
<font color="red">**GraphQLを利用して実践的なアプリを作れるスキルが身につきます。**</font>

チュートリアルはインプットであり、本当にスキルとして身につけるならアウトプットをしないといけません。アウトプットとして個人開発に導入できるようになるべく実践できるように解説していきます。

# 動画教材もご用意してます

こちらの教材にはより詳しく解説した動画もセットでご用意しています。
テキスト教材ではわからない細かい箇所があれば動画も活用ください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/PzBsTUZo12s?si=Nm2O0kNJaTUt9LKR" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


# 対象者

- GraphQLという名前を聞いたことがある人
- GraphQLをやったことがあるが身につけられなかった人
- REST APIとの違いを知りたい人
- 手を動かしてスキルを身に着けたい人
- TypeScriptを使ってみたい人
- Reactをやったことがない人
- JavaScriptはなんとなく理解している人

# 1. REST VS GraphQL

GraphQLを実際に開発する前にREST APIとの違いを理解しておくことで、GraphQLをより理解しやすくなるので解説していきます。

まずは**REST API**の特徴から紹介します。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/c7c9573a-af80-58b4-380e-a4f889698970.png)

REST APIでは`/api/user`や`/api/user/1`などリソースがURLで表現されています。
このような仕組みにするとURLが識別しやすかったり、HTTPメソッドでリソースを操作できるメリットはあるもののいくつかのデメリットがありました。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/35cc9b2b-c8fa-0310-63e2-f81e5089249b.png)

1つ目にあげられるのが、**オーバーフェッチ**の問題です。
あなたがニュース表示をアプリに追加しようと考えたとします。社内にはニュースを返すようなAPIがあったとして、それを使えば解決できることがわかりました。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/d7cbe312-49c1-5fad-1af4-680553f3f0ba.png)

ニュースAPIを使ってリリースしたところ、各所から **「アプリが遅くなった」** という連絡がくるようになりました。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/52d0eb59-89c2-895b-25b7-9ca1875f40aa.png)
　

調査をしたところ、今回は利用していない**ニュース画像**がかなり大きなデータ量になっていて、必要のない項目を取得するのに時間がかかっていたことが判明しました。
この場合、REST APIのエンドポイントを新たに追加しないといけないので工数がかかってしまいます。


もう一つのよくある問題点は、**異なるエンドポイントを叩くときに大変**というところです。マイクロサービスなどを採用するとよく起きる問題だと私も感じています。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/3e5277a7-cf82-85ee-2044-40925dc3af0f.png)

たとえば、先程のニュースの情報にニュースの発行元の情報も一緒に表示しようとします。
するとこのように別APIを2つ叩いてJSONを1つにしてフロント側に返す必要があります（BFFの役割をしたAPIを作る場合もありこれもコストがかかります)

　
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/689f06fc-f216-ff4c-1ab1-bb381551a5dc.png)

そんなREST APIのデメリットを解消したのが<font color="red">**GraphQL**</font>です。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/9981d0a1-d79b-c7bb-8c15-df97f5c25d50.png)
　

GraphQLはREST APIとは異なり、単一のエンドポイントしかありません。
このエンドポイントに対してクエリを投げることでほしいデータのみを取得することができます。先程のニュースを取得する例だと画像は必要ないので含めないこともできるのです。

そして複数のエンドポイントを叩かなくてもクエリによって、ニュースの情報と発行元の情報をまとめて取得することもできます。

ここまで聞くと魔法のように思えますが、いくつかのデメリットもあるので紹介します。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/2d955e9a-0737-3e88-219d-74a7ecc2a045.png)

いくつかデメリットはあるのですが、特に**学習コスト**がネックになることが多いです。普段使っているのはREST APIだと思うのでプロジェクトにメンバーが加わったときに学習コストが毎回かかってしまうというのはよくある話です。

これらのメリット、デメリットを考えながらプロジェクト似合いそうな手段を選択していただきたいです。

とはいえ、GraphQLを知っている/知っていないでは技術選択の可能性が大きく変わるのでぜひ一度学んでほしいです。このチュートリアルでは**学習コスト**を大幅に下げられるようにわかりやすく実践で必要な箇所に絞って体系的に解説していきます。

# 2. GraphQLの環境構築をしよう

まずはGraphQLを気軽に試せるApollo Clientを0から構築して基本的なクエリを学んでいきます。

```
$ cd graphql-react
$ mkdir graphql-server
$ cd graphql-server
```

VSCodeで開いて設定していきます。

```
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (graphql-react) 
version: (1.0.0) 
description: 
entry point: (index.js) 
test command: 
git repository: 
keywords: 
author: 
license: (ISC) 
About to write to /home/jinwatanabe/workspace/qiit/graphql-react/package.json:

{
  "name": "graphql-react",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": ""
}


Is this OK? (yes) yes

// Enterを押し進めて、最後にyesと入力
```

今回はTypeScriptで構築していくので必要なライブラリをインストールしていきます。

```
$ npm install -dev nodemon ts-loader ts-node typescript
```

次にTypeScriptのコンパイル設定を記述していきます。

```
$ touch tsconfig.json
```

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "lib": ["es2020"],
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "moduleResolution": "node",
    "baseUrl": "src",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["dist", "node_modules"],
  "compileOnSave": false
}
```

次にnodemonの設定ファイルを作ります。

```
$ touch nodemon.json
```

```json:nodemon.json
{
  "watch": ["src"],
  "ext": "*",
  "exec": "ts-node ./src/server.ts"
}
```

教材では環境構築についてはあまり言及しないことが多いですが、今回はなぜここまでの環境構築を行っているのかも解説していきます。ここがイメージできると環境構築も簡単に行えるようになります。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/9e3ab749-94aa-4a51-48f0-b8e4eed7576b.png)

今回は`src`配下にある`server.ts`を変更したら即座にサーバーに変更が反映されるようにこのような環境構築を行っています。そこで必要になるのが**nodemon**と**ts-node**なのです。

図のように私達はこれからsrcにあるファイルを変更していきます。その都度サーバーを再起動するのは大変なのでnodemonが活躍します。またTypeSccriptはそのままではブラウザでは実行できないためts-nodeなどを利用してjsにコンパイルして実行するような仕組みになっています。
　

ここまででベースのTypeScriptの環境ができたので、GraphQLを使うためのApollo Serverを起動していきます。

```
$ npm install apollo-server graphql
```

https://www.apollographql.com/docs/apollo-server/getting-started

次に実際にApollo Serverを起動するためのスクリプトを書いていきます。

```
$ mkdir src
$ touch src/server.ts
```

```ts:server.ts
import { ApolloServer, gql } from "apollo-server";

const typeDefs = gql`
  type Query {
    message: String
  }
`;

const resolvers = {
  Query: {
    message: () => "Hello World!",
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
```

次にPackage.jsonをすこし修正して起動コマンドを作ります。
`scripts`を変更します。nodemonを使って変更を検知して、変更されたらTypeScriptをJavaScriptにコンパイルしてリロードし直しています。

```json:package.json
  "scripts": {
    "ts-node": ".\\node_modules\\.bin\\ts-node",
    "start": "NODE_ENV=development nodemon --config ./nodemon.json",
  },
```

それではサーバーを起動しましょう。

```
$ npm run start
```

[localhost:4000](http://localhost:4000)にアクセスすると以下の画面が表示されれば成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/d2856521-88b3-88b4-f52c-7621e311184b.png)

ここで`Git Commit`したくなる人もいるかと思いますが、`node_module`が`git ingore`されていないので、設定してきます。

```
$ touch .gitingnore
```

```.gitignore
node_modules
```

ここまでできたら終了です！好きなだけGitHubにPushしてください。

# 3. GraphQLのクエリについて

今回はTODOアプリを題材にGraphQLのクエリについて解説していきます。
まずは先程何気なくでてきた、`typeDefs`と`resolvers`についてです。
この2つはGraphQLを使う上でもっとも重要な要素となってきます。

**TypeDefsはGraphQLのデータ構造を定義する構成図です。**
「どんなデータがあり」「どんな関係性があるのか」を表したものです。

例えばTODOアプリであればこのようになります。

```typescript
const typeDefs = gql`
  type Todo {
    id: ID!
    title: String!
    completed: Boolean!
  }
`;
```

今回最後に完成するTODOアプリをここで紹介しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/8f2ed97c-3a48-ee76-5b45-0c8f38b4f612.png)

このアプリで必要なデータとして、
- それぞれのTODOを識別する`id`
- それぞれのTODOのタイトル`title`
- それぞれのTODOが完了したかを表すフラグ`completed`(true: 終わった、false: 終わっていない)

これらが今回のTODOアプリのデータ構造になります。

**resolversは実際にデータを取得したり処理したりするための関数です。**
TypeDefsで定義された型やフィールドごとにどのデータを返すかを指示します。resolversが実際の処理を行い結果を返します。

```ts
const resolvers = {
  Query: {
    getTodos: () => {
      return [
        {
          id: "1",
          title: "GraphQLを勉強する",
          completed: false
        },
        {
          id: "2",
          title: "Reactを勉強する",
          completed: false
        }
      ]
    }
  }
};
```

ここで`Query`と`getTodos`というのがでてきました。
Queryはデータ取得のときに使う関数をまとめるものだと考えて大丈夫です。後にでてきますが、データを作成更新削除するためのMutationというのもあります。

getTodosはTODOをすべて取得するための具体的なリゾルバ関数です。GraphQLのクエリで指定することで関数を実行することが可能です。

それでは実際にTypeDefsとResoversを設定して、クエリを叩いてみましょう。

```ts:src/server.ts
import { ApolloServer, gql } from "apollo-server";

const todos = [
  {
    id: "1",
    title: "GraphQLを勉強する",
    completed: false,
  },
  {
    id: "2",
    title: "Reactを勉強する",
    completed: false,
  },
];

const typeDefs = gql`
  type Todo {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    getTodos: [Todo!]!
  }
`;

const resolvers = {
  Query: {
    getTodos: () => todos,
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
```

ここで先程の説明にない箇所がでてきました。

```ts
const typeDefs = gql`
  type Todo {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    getTodos: [Todo!]!
  }
`;
```

TypeDefsではQueryの操作の部分(Resolversに書いたもの)も書く必要があります。ここが実際にクライアントがサーバーにリクエストできる入口となるのです。

`[Todo!]!`とありますが、これは[Todo]でTodo型(id, title, completedをもつオブジェクト)の配列が返ってくることを表しており`!`をつけることで`null`でないことを表しています。リストの各要素は必ずnullでなく`[Todo!]`、リストそのものもnullではないと定義しています`[Todo!]!`
　

それではサーバーを起動して、[localhost:4000](http://localhost:4000)を開きましょう。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/99b0c027-f425-342c-489d-31dfcdb2b2b6.png)

チェックをいれてQuery your serverをクリックします。

エディタが開くので以下を入力します。

```
query GetTodos {
  getTodos {
    id
    title
    completed
  }
}
```

そして実行ボタンを押すと右に先程のテストデータが表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/4d258d7c-5716-6ed1-ac6a-647ff453ab5c.png)

`query GetTodos`はクエリ名を表すものです。名前をつけるだけなので、GetTodosでなくHogeでも構いません。その次に実際に実行するResolversの関数を書きます。今回は`getTodos`を呼び出しました。

そして、ここからがREST APIとの違いで、必要なフィールドだけを書いていきます。
今回はすべてのフィールドを書きましたが、以下のようにするとidとtitleだけを取り出せます。

```
query GetTodos {
  getTodos {
    id
    title
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/f995cded-ec89-c63b-a50d-da1537540ee6.png)

これで図解したときのニュースが重くなってしまう問題は解消しますね！

では、続いて**TODO新規作成**をクエリを使ってやっていきます。
追加、更新、削除はQueryではなく、`Mutation`を使います。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/967998da-867c-12b2-a07d-9227aab083de.png)
　

REST APIのメソッドとの関係はこんな感じです。GET以外はMutationを使うと覚えておけばよいです。

```ts:src/server.ts
import { ApolloServer, gql } from "apollo-server";

const todos = [
  {
    id: "1",
    title: "GraphQLを勉強する",
    completed: false,
  },
  {
    id: "2",
    title: "Reactを勉強する",
    completed: false,
  },
];

const typeDefs = gql`
  type Todo {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    getTodos: [Todo!]!
  }

  type Mutation {
    addTodo(title: String!): Todo!
  }
`;

const resolvers = {
  Query: {
    getTodos: () => todos,
  },
  Mutation: {
    addTodo: (_: unknown, { title }: { title: string }) => {
      const newTodo = {
        id: String(todos.length + 1),
        title,
        completed: false,
      };
      todos.push(newTodo);
      return newTodo;
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
```

まずはresolversにMutationを追加してタイトルを受け取ってリストに追加する処理を書きました。

```ts
const resolvers = {
  Query: {
    getTodos: () => todos,
  },
  Mutation: {
    addTodo: (_: unknown, { title }: { title: string }) => {
      const newTodo = {
        id: String(todos.length + 1),
        title,
        completed: false,
      };
      todos.push(newTodo);
      return newTodo;
    },
  },
};
```

unkownになっている箇所は引数を使用しないことを示しています。anyなどにもできますが簡単に利用できていしまうため安全を考えて、明示的に確認をしないと使えないunkownにしています。
　
そしてresolversを書いたら、クライアントからの入口となる`TypeDefs`にも定義を追加します。

```ts
const typeDefs = gql`
  type Todo {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    getTodos: [Todo!]!
  }

  type Mutation {
    addTodo(title: String!): Todo!
  }
`;
```

では、実際に確認していくのでApolloクライアントを開いてください。

```
mutation AddTodo($title: String!) {
  addTodo(title: "hoge") {
    title
  }
}

query Hoge {
  getTodos {
    id
    title
    completed
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/26d5f36d-47a7-d450-0da9-0abe2fdbb075.png)

このように2つのクエリを実行することで、新しいTODOが追加できたことが確認できます。

`mutation AddTodo($title: String!)`の`$title`が気になった方もいるかもしれません
今回は直接`hoge`という文字でTODOを作成しているので必要性が感じられませんが、本来GraphQLサーバーに対して変数を送ってクエリを実行します。

```
mutation AddTodo($title: String!) {
  addTodo(title: $title) {
    id
    title
    completed
  }
}
```

```json
{
  "title": "hoge"
}
```

今回はクライアントを使って便利に実行しているので必要性は感じられませんが、このあとは`$title`を利用するクエリを使っていきます。
　

残りが更新と削除ですが、ほとんど変わらないためコードを紹介して終わります。
詳しく知りたい方は動画の方をご確認ください。

```ts:src/server.ts
import { ApolloServer, gql } from "apollo-server";

const todos = [
  {
    id: "1",
    title: "GraphQLを勉強する",
    completed: false,
  },
  {
    id: "2",
    title: "Reactを勉強する",
    completed: false,
  },
];

const typeDefs = gql`
  type Todo {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    getTodos: [Todo!]!
  }

  type Mutation {
    addTodo(title: String!): Todo!
    updateTodo(id: ID!, completed: Boolean!): Todo!
    deleteTodo(id: ID!): Todo!
  }
`;

type AddTodo = {
  title: string;
};

const resolvers = {
  Query: {
    getTodos: () => todos,
  },
  Mutation: {
    addTodo: (_: unknown, { title }: AddTodo) => {
      const newTodo = {
        id: String(todos.length + 1),
        title,
        completed: false,
      };
      todos.push(newTodo);
      return newTodo;
    },
    updateTodo: (
      _: unknown,
      { id, completed }: { id: string; completed: boolean }
    ) => {
      const todo = todos.find((todo) => todo.id === id);
      if (!todo) {
        throw new Error("Todo not found");
      }
      todo.completed = completed;
      return todo;
    },
    deleteTodo: (_: unknown, { id }: { id: string }) => {
      const index = todos.findIndex((todo) => todo.id === id);
      if (index === -1) {
        throw new Error("Todo not found");
      }
      const deletedTodo = todos.splice(index, 1);
      return deletedTodo[0];
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

```

更新のクエリは以下を実行します。ここでサーバーが再起動するとTODOが初期の状態に戻る可能性もあるため、一応新規追加しておきます。

```
mutation AddTodo($title: String!) {
  addTodo(title: "hoge") {
    title
  }
}

mutation Update($updateTodoId: ID!, $completed: Boolean!) {
  updateTodo(id: "3", completed: true) {
    id
    title
    completed
  }
}

query Hoge {
  getTodos {
    id
    title
    completed
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/438745cd-d375-7418-320e-7005c7360ff0.png)


削除は以下のクエリで行います。タスクhogeを消します。

```
mutation AddTodo($title: String!) {
  addTodo(title: "hoge") {
    id
    title
    completed
  }
}

mutation DeleteTodo($deleteTodoId: ID!) {
  deleteTodo(id: "3") {
    id
    title
    completed
  }
}


query Hoge {
  getTodos {
    id
    title
    completed
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/f01fdba1-9e89-ef21-4b31-090667367d7f.png)

ここまででGraphQLnの基本が学べたので次はデータの永続化をするためにデータベースを導入していきます。

# 4. データの永続化をする

ここからは**Prisma**と**SQLite**でデータの永続化をしていきます。
先程作ったサーバーではデータの永続化ができておらずサーバーを再起動するとTODOの状態がリセットされて初期状態(2つのTODOがある)に戻ってしまいます。

そこで利用するのが、PrismaとSQliteです。

**Prismaはデータベースとのやり取りを簡単にするツールです。**
ORMとして使われます。ORMとはデータベースのテーブルをオブジェクトとして操作できる技術で、SQLを書かなくてもJavaScriptのコードだけでデータベース操作ができるようになります。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/86a4b4b9-131f-b2fa-d9ec-e96da470dbd5.png)

Prismaを使えば`prisma.user.create`だけで新しいユーザーをDBに追加することができます。
　

**SQLiteは軽量で組み込み型のリレーショナルデータベースです。**
アプリの中でデータベースを持てるので外部にサーバーを立てる必要がありません。小規模なプロジェクトでよく使われます。
　

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/7ab44b87-7cc2-9f04-deb8-d9c34fb0cf69.png)

本来DBを用意するのであればAWSなどクラウドサービスを利用したり、Dockerでコンテナを立てたりコストがかかりますが、SQLiteはアプリケーション内に用意でき、ファイルベースとなっているので簡単に利用が可能です。

それでは実際にSQliteを利用してデータの永続化をしてみます。
まずはPrismaをインストールしましょう。

https://www.prisma.io/docs/getting-started/quickstart-sqlite

```
$ npm install prisma --save-dev
$ npx prisma init --datasource-provider sqlite
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/2b80cdaa-ed2b-16e2-017c-a70e164d1b1c.png)

prismaというディレクトリができました。
中にある`schema.prisma`を修正します。

```src/prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db" 
}

model Todo {
  id        String   @id @default(cuid())
  title     String
  completed Boolean
  createdAt DateTime @default(now())
}
```

`url`を`file:./dev.db`としました。これによりDBの情報がschema.prismaと同じ位置にファイルとして生成されます。

```
model Todo {
  id        String   @id @default(cuid())
  title     String
  completed Boolean
  createdAt DateTime @default(now())
}
```

は先程のTypeDefsの内容を入れています。新しくcreatedAtを追加しました。
`@id @default(cuid())`とすることで自動でUUIDをIDに採番してくれます。
`@default(now())`でデフォルトはテーブル作成の時間を設定してくれます。

それではマイグレートを行っていきます。マイグレートとは今設定した内容を実際のデータベースに反映させる作業です。

```
$ npx prisma migrate dev --name init
```

ここまで行うと色々設定ファイルが作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/ad2ed1c0-05a7-753e-6215-0299422bac63.png)

migratitonsの中にはマイグレートの履歴のようなものが残っています。
dev.dbは先程設定した項目でDBの情報を保持しているファイルになります。

これでPrismaとSQLiteの設定が終わりました。早速GraphQLとDBを接続していきましょう！
といっても実はそこまで難しくないです！

```ts:src/server.ts
import { PrismaClient } from "@prisma/client";
import { ApolloServer, gql } from "apollo-server";

const prisma = new PrismaClient();

type Context = {
  prisma: PrismaClient;
}

const typeDefs = gql`
  type Todo {
    id: ID!
    title: String!
    completed: Boolean!
  }

  type Query {
    getTodos: [Todo!]!
  }

  type Mutation {
    addTodo(title: String!): Todo!
    updateTodo(id: ID!, completed: Boolean!): Todo!
    deleteTodo(id: ID!): Todo!
  }
`;

type AddTodo = {
  title: string;
};

const resolvers = {
  Query: {
    getTodos: async (_: unknown, args: any, context: Context) => {
      return await context.prisma.todo.findMany();
    },
  },
  Mutation: {
    addTodo: (_: unknown, { title }: AddTodo, context: Context) => {
      return context.prisma.todo.create({
        data: {
          title,
          completed: false,
        },
      });
    },
    updateTodo: (
      _: unknown,
      { id, completed }: { id: string; completed: boolean },
      context: Context
    ) => {
      return context.prisma.todo.update({
        where: { id },
        data: { completed },
      });
    },
    deleteTodo: (_: unknown, { id }: { id: string }, context: Context) => {
      return context.prisma.todo.delete({
        where: { id },
      });
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({ prisma }),
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});
```

コードがPrismaを利用することでスッキリしました！

まず修正したのはテストデータをなくしました。

```ts
// データベースを使うため不要
const todos = [
  {
    id: "1",
    title: "GraphQLを勉強する",
    completed: false,
  },
  {
    id: "2",
    title: "Reactを勉強する",
    completed: false,
  },
];
```

そしてPrisma Clientの初期設定を行います。

```ts
const prisma = new PrismaClient();

type Context = {
  prisma: PrismaClient;
}
```

やっていることはPrismaクライアントを使う初期化(インスタンス生成)と、このあとresolversの中で`context.prisma`を使うための型を宣言しています。

```ts
    getTodos: async (_: unknown, args: any, context: Context) => {
      return await context.prisma.todo.findMany();
    },
```

ここで`context: Context`と受け取り、`context.prisma`と呼び出しています。
contextの型をはっきり書いてあげることでcontextにはprismaが呼び出せるということをTypeScriptにわからせてあげる必要があるために型宣言をしました。

```ts
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({ prisma }),
});
```

次にApollo Serverにcontextという引数でresolversにprismaを渡せるように設定します。

```ts
    getTodos: async (_: unknown, args: any, context: Context) => {
      return await context.prisma.todo.findMany();
    },
```

あとはgetTodosであれば、prismaの便利な関数である`findMany`を使ってすべてのレコードを取得します。
ドキュメントをみると操作に対してどのようにprismaを呼べば良いのかが書いてあります！

https://www.prisma.io/docs/orm/prisma-client/queries/crud

**それではデータが永続化できているかを確認します。**
サーバーを起動し直してすべてのクエリを確かめます。


```
mutation AddTodo($title: String!) {
  addTodo(title: "hoge") {
    id
    title
    completed
  }
}

query Hoge {
  getTodos {
    id
    title
    completed
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/c52bfb40-e304-0fc6-7f29-41fccb1d8603.png)

データが追加できたことを確認できたら一度サーバーを再起動します。

```
$ npm run start
```

そしてデータを確認しましょう。

```
query Hoge {
  getTodos {
    id
    title
    completed
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/43afc6a0-c7f4-3485-cdab-de068d0f27d7.png)

しっかり永続化がされていそうです！(私は2回追加したので2つになっています)

それでは他のクエリも正しく動くかを確認しましょう

```
mutation Update($updateTodoId: ID!, $completed: Boolean!) {
  updateTodo(id: "あなたの追加したTODOのUUIDをいれる", completed: true) {
    id
    title
    completed
  }
}

mutation DeleteTodo($deleteTodoId: ID!) {
  deleteTodo(id: "あなたの追加したTODOのUUIDをいれる") {
    id
    title
    completed
  }
}
```

注意点はIDが今回からPrismaのSchemaで設定した自動採番によるUUIDになっていることです。
テストデータでは連番で入れていましたが変わるのでご自身で設定してクエリを実行してください

```prisma:src/prisma/schema.prisma
model Todo {
  id        String   @id @default(cuid()) ← これ
  title     String
  completed Boolean
  createdAt DateTime @default(now())
}
```

更新削除が確認できたらPrismaとGraphQLに関してはこれにて終わりです。
今回のチュートリアルはここでは終わりません。実際にサンプルアプリを構築して組み込みたいと思います。

あとでデータを使いたいので以下のクエリでテストデータを入れておきましょう

```
mutation AddTodo($title: String!) {
  addTodo(title: "お昼ご飯をつくる") {
    id
    title
    completed
  }
}
```


# 5. Reactの環境構築

ここからはReactで簡単なTODOアプリを作成してGraphQLを叩いてデータを取得更新していきます。

Reactとかっこいい画面を簡単に作れるコンポーネントライブラリShadcnが実行できる環境を用意します。
Node.jsが実行できる環境がお手元にない方は以下を参考にそれぞれのOSにあった方法でインストールしてください！

https://nodejs.jp/

https://www.sejuku.net/blog/72545

インストールができたことを以下の確認で確認してください

```
❯ node -v
v18.17.0
```

Reactのプロジェクトを構築します。
今回はViteを利用していきます。Viteは次世代のビルドツールで早くて無駄のない環境を提供してくれます。

https://ja.vitejs.dev/guide/

```
cd ..
```

最初に作成したディレクトリ`graphql-react`へ移動してください。

```
❯ npm create vite
Need to install the following packages:
  create-vite@5.5.2
Ok to proceed? (y) y
✔ Project name: … todo-client
✔ Select a framework: › React
✔ Select a variant: › TypeScript

$ cd todo-clientcd/
$ npm i
$ npm run dev
```

[http://localhost:5173](http://localhost:5173)にアクセスして以下の画面が表示されればReact環境が無事できています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/cc74191c-f3a2-533b-93c4-97c5fb0a8d9b.png)


次にShadcnを利用するために、Shadcnの内部で利用しているTailwindCSSを導入していきます。

```
$ npm install -D tailwindcss@3.4.13 postcss autoprefixer
$ npx tailwindcss init -p
```

プロジェクトをVSCodeで開いて`tailwind.config.js`を以下に変えます

```js:tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

`src/index.css`を変更します

```css:src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

`src/App.tsx`を変更してスタイルがあたるかをチェックします

```tsx:src/App.tsx
function App() {
  return (
    <>
      <div>
        <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
          Button
        </button>
      </div>
    </>
  );
}

export default App;
```

一度サーバーを落として`npm run dev`で起動したらボタンが表示されました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/a844a950-e881-3dac-16cb-792dd2c4f430.png)

TailwindCSSが導入できたので、Shadcnを導入します。

https://ui.shadcn.com/docs/installation/vite

`tsconfig.json`を以下に修正します。

```json:tsconfig.json
{
  "files": [],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ],
    "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
```

`tsconfig.app.json`を修正します。

```json:tsconfig.app.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "isolatedModules": true,
    "moduleDetection": "force",
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "./src/*"
      ]
    }
  },
  "include": ["src"]
}
```

次に以下を実行します。

```
$ npm i -D @types/node
```

そして`vite.config.ts`を修正します。

```ts:vite.config.ts
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})
```

それでは準備ができたので実際にShadcnを初期化して入れていきます。

```
$ npx shadcn@latest init

Need to install the following packages:
shadcn@2.1.0
Ok to proceed? (y) y
✔ Preflight checks.
✔ Verifying framework. Found Vite.
✔ Validating Tailwind CSS.
✔ Validating import alias.
✔ Which style would you like to use? › New York
✔ Which color would you like to use as the base color? › Neutral
✔ Would you like to use CSS variables for theming? … no / yes
✔ Writing components.json.
✔ Checking registry.
✔ Updating tailwind.config.js
✔ Updating src/index.css
✔ Installing dependencies.
✔ Created 1 file:
  - src/lib/utils.ts

Success! Project initialization completed.
You may now add components.
```

試しにボタンコンポーネントをダウンロードしてきて、使ってみます。

```
$ npx shadcn@latest add button
```

```tsx:App.tsx
import { Button } from "./components/ui/button";

function App() {
  return (
    <>
      <div>
        <Button>Click me</Button>
      </div>
    </>
  );
}

export default App;
```

画面を確認してみます。`npm run dev`でサーバーを起動した状態で確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/98dff88c-bd65-b80f-c947-7b2028688922.png)

環境構築がすべて終わりました。

# TODOアプリを構築する

まずは`App.tsx`にGraphQLからデータを取得して表示するところまで実装していきます。
いきなりGraphQLと接続すると難しい方もいるかと思うのでテストデータを表示するところから始めます。

```tsx:src/App.tsx
type Todo = {
  id: string;
  title: string;
  completed: boolean;
};

function App() {
  const todos = [
    { id: "1", title: "GraphQLを学ぶ", completed: false },
    { id: "2", title: "Reactを学ぶ", completed: true },
  ] as Todo[];

  return (
    <>
      <div>
        <h1>TO DO List</h1>
        <input type="text" placeholder="TODOを追加してください" />
        <button>追加</button>
        <ul>
          {todos.map((todo) => (
            <li
              key={todo.id}
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
            >
              <input type="checkbox" checked={todo.completed} />
              {todo.title}
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

export default App;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/4000387c-155a-e5d9-1317-af8ff770350d.png)

それではGraphQLを使って実際のデータベースのデータを取得しましょう。
ReactでApolloが使えるように初期設定をします。こちらはドキュメント通りにやります。

https://www.apollographql.com/docs/react/get-started

```
$ npm install @apollo/client graphql
```

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

const client = new ApolloClient({
  uri: "http://localhost:4000",
  cache: new InMemoryCache(),
});

ReactDOM.createRoot(document.getElementById("root")!).render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);
```

接続するサーバーを初期設定しています。今回はローカルのサーバー(localhost:4000)です。

```tsx
const client = new ApolloClient({
  uri: "http://localhost:4000",
  cache: new InMemoryCache(),
});
```


それでは実際にGraphQLでTODOの一覧を取得したコードを紹介していきます。

```tsx:src/App.tsx
import { gql, useQuery } from "@apollo/client";

const GET_TODOS = gql`
  query {
    getTodos {
      id
      title
      completed
    }
  }
`;

type Todo = {
  id: string;
  title: string;
  completed: boolean;
};

function App() {
  const { loading, data } = useQuery(GET_TODOS, {
    fetchPolicy: "network-only",
  });

  const todos = data ? data.getTodos : [];

  if (loading) return <p>Loading...</p>;

  return (
    <>
      <div>
        <h1>TO DO List</h1>
        <input type="text" placeholder="TODOを追加してください" />
        <button>追加</button>
        <ul>
          {todos.map((todo: Todo) => (
            <li
              key={todo.id}
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
            >
              <input type="checkbox" checked={todo.completed} />
              {todo.title}
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

export default App;

```

ここで先ほど作成したサーバーも起動しておきましょう

```
graphql-serverディレクトリで起動する
$ npm run start
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/e9b05565-179a-156a-43de-1e6a71d49daa.png)

ではコードを解説していきます。

```tsx
const GET_TODOS = gql`
  query {
    getTodos {
      id
      title
      completed
    }
  }
`;
```

先程叩いたクエリと同じものを用意しました。

```tsx
  const { loading, data } = useQuery(GET_TODOS, {
    fetchPolicy: "network-only",
  });

  const todos = data ? data.getTodos : [];

  if (loading) return <p>Loading...</p>;
```

`@apollo/client`は`useQuery`という便利な関数(フック)を用意してくれます。
この関数にクエリを渡して実行するとデータが返ってきたタイミングで画面を再描画してくれます。
`fechPolicy`はキャッシュをどうするかを選べるものです。

https://qiita.com/yodog712/items/bc762e8ad0ea01c06ce8

データが返ってこない最初のタイミングでは`data.getTodos`が空となり、**loading**という文字がでます。
データが取得できると`data.getTodos`に値が入るのでTODOリストが表示されます。

```tsx
          {todos.map((todo: Todo) => (
            <li
              key={todo.id}
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
            >
              <input type="checkbox" checked={todo.completed} />
              {todo.title}
            </li>
          ))}
```

それでは**追加**と**更新**も同じ要領でやっていきましょう！

```tsx:src/App.tsx
import { gql, useMutation, useQuery } from "@apollo/client";
import { useState } from "react";

const GET_TODOS = gql`
  query {
    getTodos {
      id
      title
      completed
    }
  }
`;

const ADD_TODO = gql`
  mutation addTodo($title: String!) {
    addTodo(title: $title) {
      id
      title
      completed
    }
  }
`;

const UPDATE_TODO = gql`
  mutation updateTodo($id: ID!, $completed: Boolean!) {
    updateTodo(id: $id, completed: $completed) {
      id
      title
      completed
    }
  }
`;

type Todo = {
  id: string;
  title: string;
  completed: boolean;
};

function App() {
  const { loading, data } = useQuery(GET_TODOS, {
    fetchPolicy: "network-only",
  });

  const todos = data ? data.getTodos : [];
  const [addTodo] = useMutation(ADD_TODO);
  const [updateTodo] = useMutation(UPDATE_TODO);
  const [title, setTitle] = useState("");

  if (loading) return <p>Loading...</p>;

  const handleAddTodo = async () => {
    await addTodo({
      variables: { title },
      refetchQueries: [{ query: GET_TODOS }],
    });
    setTitle("");
  };

  const handleUpdateTodo = async (id: string, completed: boolean) => {
    await updateTodo({
      variables: { id, completed: !completed },
      refetchQueries: [{ query: GET_TODOS }],
    });
  };

  return (
    <>
      <div>
        <h1>TO DO List</h1>
        <input
          type="text"
          placeholder="TODOを追加してください"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        <button onClick={handleAddTodo}>追加</button>
        <ul>
          {todos.map((todo: Todo) => (
            <li
              key={todo.id}
              style={{
                textDecoration: todo.completed ? "line-through" : "none",
              }}
            >
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => handleUpdateTodo(todo.id, todo.completed)}
              />
              {todo.title}
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

export default App;
```

新しいTODOを追加してみました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/ccd64e77-b23c-ad53-ffc2-c10af463b424.png)

TODOを更新してみました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/1da8d4a7-adb5-0cd0-4f7a-82a476d637fe.png)

画面をリロードしても同じデータであれば成功です。
それではコードを解説していきます。

```tsx
const ADD_TODO = gql`
  mutation addTodo($title: String!) {
    addTodo(title: $title) {
      id
      title
      completed
    }
  }
`;

const UPDATE_TODO = gql`
  mutation updateTodo($id: ID!, $completed: Boolean!) {
    updateTodo(id: $id, completed: $completed) {
      id
      title
      completed
    }
  }
`;
```

先程GraphQLで確かめたクエリと同じものを用意しました。

```tsx
  const [addTodo] = useMutation(ADD_TODO);
  const [updateTodo] = useMutation(UPDATE_TODO);
  const [title, setTitle] = useState("");
```

こちらも同じく`useQuery`の代わりに`useMutation`を使っています。
**title**は追加のときに入力されている値を使いたいので、`useState`を使ってフォームの値を保存しています。

```tsx
  const handleAddTodo = async () => {
    await addTodo({
      variables: { title },
      refetchQueries: [{ query: GET_TODOS }],
    });
    setTitle("");
  };

  const handleUpdateTodo = async (id: string, completed: boolean) => {
    await updateTodo({
      variables: { id, completed: !completed },
      refetchQueries: [{ query: GET_TODOS }],
    });
  };
```

前とさほど変わりませんが、**variables**に変数を送っています。
addTodoではステートに保存されているタイトルを入れており、updateTodoでは現在のcompletedを逆に(trueならfalse)して送っています。

そのあと`refechQueries`を実行しています。これはクエリが実行された後に自動で実行されるクエリでTODOの一覧を改めて取得して画面を更新しています。

```tsx
        <input
          type="text"
          placeholder="TODOを追加してください"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
```

titleは入力されたらuseStateの`set`関数を使ってタイトルを更新しています。
onChangeは入力が変更されたら実行されます。

```tsx
              <input
                type="checkbox"
                checked={todo.completed}
                onChange={() => handleUpdateTodo(todo.id, todo.completed)}
              />
```

checkboxもonChangeを使っていますが、チェックされたらすぐにTODOのデータベースも更新するので`handleUpdateTodo`を呼び出しています。呼び出されたTODOのIdとCompletedを関数に渡しています。

```tsx
       <button onClick={handleAddTodo}>追加</button>
```

追加ボタンには`onClick`を設定しました。これはクリックされたときにどんな処理をするのかを設定できます。ここでは`handleAddTodo`を呼び出しています。

# スタイルを整える

最後にかっこいいデザインにして終わりにしましょう。
ここではFramer Motionというアニメーションライブラリも利用していますので、気になる方は調べてみてください。

https://www.framer.com/motion/

```
$ npm install framer-motion
```

Shadcn/uiのコンポーネントも追加しておきます。

```
$ npx shadcn@latest add checkbox
```

```tsx:/src/App.tsx
import { gql, useMutation, useQuery } from "@apollo/client";
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { CheckCircle2, PlusCircle } from "lucide-react";
import { Button } from "./components/ui/button";
import { Checkbox } from "./components/ui/checkbox";

const GET_TODOS = gql`
  query {
    getTodos {
      id
      title
      completed
    }
  }
`;

const ADD_TODO = gql`
  mutation addTodo($title: String!) {
    addTodo(title: $title) {
      id
      title
      completed
    }
  }
`;

const UPDATE_TODO = gql`
  mutation updateTodo($id: ID!, $completed: Boolean!) {
    updateTodo(id: $id, completed: $completed) {
      id
      title
      completed
    }
  }
`;

type Todo = {
  id: string;
  title: string;
  completed: boolean;
};

function App() {
  const { loading, data } = useQuery(GET_TODOS, {
    fetchPolicy: "network-only",
  });

  const todos = data ? data.getTodos : [];
  const [addTodo] = useMutation(ADD_TODO);
  const [updateTodo] = useMutation(UPDATE_TODO);
  const [title, setTitle] = useState("");

  if (loading) return <p>Loading...</p>;

  const handleAddTodo = async () => {
    await addTodo({
      variables: { title },
      refetchQueries: [{ query: GET_TODOS }],
    });
    setTitle("");
  };

  const handleUpdateTodo = async (id: string, completed: boolean) => {
    await updateTodo({
      variables: { id, completed: !completed },
      refetchQueries: [{ query: GET_TODOS }],
    });
  };

  return (
    <>
      <div className="min-h-screen bg-gradient-to-br from-teal-50 to-mint-100 flex items-center justify-center p-4">
        <motion.div
          initial={{ opacity: 0, y: -20 }}
          animate={{ opacity: 1, y: 0 }}
          className="w-full max-w-md bg-white rounded-xl shadow-2xl overflow-hidden"
        >
          <div className="bg-gradient-to-r from-teal-400 to-emerald-500 p-6">
            <h1 className="text-3xl font-bold text-white mb-2">To-Do List</h1>
          </div>
          <div className="p-6">
            <div className="flex mb-4">
              <input
                type="text"
                placeholder="タスクを追加"
                value={title}
                onChange={(e) => setTitle(e.target.value)}
                className="flex-grow mr-2 bg-teal-50 border-teal-200 focus:ring-2 focus:ring-teal-300 focus:border-transparent"
              />
              <Button
                onClick={handleAddTodo}
                className="bg-emerald-500 hover:bg-emerald-600 text-white"
              >
                <PlusCircle className="w-5 h-5" />
              </Button>
            </div>

            <AnimatePresence>
              {todos.map((todo: Todo) => (
                <motion.div
                  key={todo.id}
                  initial={{ opacity: 0, y: 20 }}
                  animate={{ opacity: 1, y: 0 }}
                  exit={{ opacity: 0, y: -100 }}
                  transition={{ duration: 0.3 }}
                  className={`flex items-center mb-4  p-4 rounded-lg shadow-sm ${
                    todo.completed ? "bg-mint-100" : "bg-white"
                  }`}
                >
                  <Checkbox
                    id={`todo-${todo.id}`}
                    checked={todo.completed}
                    onCheckedChange={() =>
                      handleUpdateTodo(todo.id, todo.completed)
                    }
                    className="mr-3 border-teal-400 text-teal-500"
                  />
                  <label
                    htmlFor={`todo-${todo.id}`}
                    className={`flex-grow text-lg ${
                      todo.completed
                        ? "line-through text-teal-600"
                        : "text-gray-800"
                    }`}
                  >
                    {todo.title}
                  </label>
                  {todo.completed && (
                    <CheckCircle2 className="w-5 h-5 text-teal-500 ml-2" />
                  )}
                </motion.div>
              ))}
            </AnimatePresence>
            {todos.length === 0 && (
              <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                className="text-center text-teal-600 mt-6"
              >
                タスクがありません
              </motion.div>
            )}
          </div>
        </motion.div>
      </div>
    </>
  );
}

export default App;
```

![バズグルメ誕生 (12).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/fa45c003-d444-0fa4-6f8d-57ae04e1c93b.gif)

# おわりに

いかがでしたでしょうか？
GraphQLはなかなか身につかないと私自身悩んでいましたが、実践的に学習することで利用するイメージがついたと思います。

テキストでは細かく解説しましたが、一部説明しきれていない箇所もありますので動画教材で紹介しています。合わせてご利用ください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/PzBsTUZo12s?si=Nm2O0kNJaTUt9LKR" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

https://github.com/jinwatanabe/graphql-beginners-course

ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。

また明日の記事でお会いしましょう！

# JISOUのメンバー募集中！

プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか？
興味のある方は、ぜひホームページをのぞいてみてくださ！
▼▼▼

https://projisou.jp