ReactでGitHub APIを叩いて自分のbio, statusを更新する

# はじめに
GitHubが提供する`GitHub REST API`と`GitHub GraphQL API`を叩いて、
`REST API`と`GraphQL API`の違いを確認しつつ、
自分のbio（自己紹介文）やステータスを更新したいと思います。

## 環境構築
あえて`create-react-app`は使わずに環境構築します（勉強になりそう）。

```terminal:ターミナル
$ node -v
v16.13.0

$ npm -v
8.1.0

$ mkdir github-api
$ cd github-api

$ npm init -y
$ npm i react react-dom typescript
$ npm i -D webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel/preset-env @babel/preset-react @babel/preset-typescript @types/react @types/react-dom

$ gibo dump Node macOS >> .gitignore

$ npx tsc --init

// ローカルサーバ起動
$ npx webpack serve
```

```js:webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: './src/App.tsx',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  resolve: {
    extensions: [".js", ".ts", ".tsx"],
  },
  module: {
    rules: [
      {
        test: [/\.ts$/, /\.tsx$/],
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        }
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "src/index.html")
    })
  ],
  devServer: {
    static: {
      directory: path.join(__dirname, "dist"),
    },
  },
};
```

```js:babel.config.js
module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react',
    '@babel/preset-typescript'
  ],
  targets: '> 1.5%, last 2 versions, not dead',
};
```

```json:package.json（一部抜粋）
{
  一部省略

  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "typescript": "^4.5.2"
  },
  "devDependencies": {
    "@babel/preset-env": "^7.16.4",
    "@babel/preset-react": "^7.16.0",
    "@babel/preset-typescript": "^7.16.0",
    "@types/react": "^17.0.37",
    "@types/react-dom": "^17.0.11",
    "babel-loader": "^8.2.3",
    "html-webpack-plugin": "^5.5.0",
    "webpack-cli": "^4.9.1",
    "webpack-dev-server": "^4.6.0"
  }
}
```

```json:tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}
```

# GitHub API 概要
2種類のAPIを使ってGitHubのbio, statusを更新してみます。

https://docs.github.com/ja/rest

https://docs.github.com/ja/graphql

今回は、`REST API`では`PATCH`リクエスト、`GraphQL API`では`POST`リクエストを送信します。
そのため、認証を求められますが、GitHubで個人アクセストークンを作成するのは簡単です。
GitHubにログインして「Settings > Developper settings > Personal access tokens」と遷移します。
「Generate new token」ボタンからトークンを作成できます。
作成したトークンをHTTPリクエストの`Authorization`ヘッダーに付加してリクエストを送信します。
今回はbio, statusを更新するだけなので、アクセス権のスコープは以下のように`user`のみにしました。
![token_scopes.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/00d285fb-f2ae-fada-82b9-ed56047d79f7.png)


# GitHub REST API を使った実装
`GitHub Docs`によると、REST APIのバージョンを指定したほうがいいそうです。

> デフォルトでは、https://api.github.com へのすべてのリクエストが REST API の v3 バージョンを受け取ります。 Accept ヘッダを介してこのバージョンを明示的にリクエストすることをお勧めします。

なので、fetchを使ったリクエストは以下のようになりました。

```tsx:Rest.tsx
import React, { useState, FormEvent } from "react";

export const Rest = () => {
  const token = '作成した個人アクセストークン';
  const [data, setData] = useState();
  const [bio, setBio] = useState<string>("");
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    fetch('https://api.github.com/user', {
      method: "PATCH",
      body: JSON.stringify({ bio }),
      // 上記は、body: JSON.stringify({ 'bio': bio }), と等価
      headers: {
        Accept: "application/vnd.github.v3+json",
        Authorization: `bearer ${token}`,
      },
    })
      .then(res => res.json())
      .then(setData)
      .catch(console.error)
  }

  return (
    <>
      <h1>GitHub REST API で bioを更新</h1>
      <form onSubmit={handleSubmit}>
        <textarea onChange={e => setBio(e.target.value)} cols={50} rows={3} placeholder="your bio..." required />
        <button>更新する</button>
      </form>
      {data && <p>{`bioを「${data["bio"]}」に更新しました。`}</p>}
    </>
  )
}
```

ただのデモなのでアクセストークンを直書きしていますが、本来であれば、
ルートディレクトリに置いた`.env`ファイルにトークンを書いて、`dotenv-webpack`など、
webpackのコンパイル時に`.env`ファイルに書いた環境変数をインポートするプラグインを使うのが良さそうです。
（`create-react-app`を使ってReactのプロジェクトを作った場合は、環境変数を設定するために必要な`dotenv`というパッケージが予めインストールされているみたいです。）

```tsx:App.tsx
import React from "react";
import ReactDOM from "react-dom";

import { Rest } from "./rest";

ReactDOM.render(
  <React.StrictMode>
    <Rest />
  </React.StrictMode>,
  document.getElementById('root')
);
```

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>GitHub API test</title>
</head>
<body>
    <div id="root" />
</body>
</html>
```

![qiita_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/4741ae45-34d4-e9f8-3962-d1bf3284b899.gif)

# GitHub GraphQL API を使った実装
続いて`GraphQL API`を使ってGitHubのステータスを更新します。
ステータスのメッセージや絵文字を更新できるみたいです。
`REST API`との違いは、やはり単一のエンドポイントを持つというところでしょうか。

> GraphQLのエンドポイント
REST APIは多数のエンドポイントを持ちますが、GraphQL APIは単一のエンドポイントを持ちます。
https://api.github.com/graphql
行う操作にかかわらず、エンドポイントは一定のままです。

GraphQLを利用するためのReact用ライブラリ等は使わず、
fetchを使って、リクエストの`body`に`query`, `variables`を渡そうと思います。
以下を参考にしました。

https://graphql.org/graphql-js/graphql-clients/

```tsx:Graphql.tsx
import React, { useState, FormEvent } from "react";

export const Graphql = () => {
  const token = '作成した個人アクセストークン';
  const [data, setData] = useState();
  const [message, setMessage] = useState<string>("");
  const [emoji, setEmoji] = useState<string>("");
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    fetch('https://api.github.com/graphql', {
      method: "POST",
      body: JSON.stringify({
        query: `
          mutation updateStatusMutation($message: String!, $emoji: String!) {
            changeUserStatus(input: {message: $message, emoji: $emoji}) {
              status {
                message
                emoji
              }
            }
          }
        `,
        variables: {message, emoji}
      }),
      headers: {
        Authorization: `bearer ${token}`,
      },
    })
      .then(res => res.json())
      .then(setData)
      .catch(console.error)
  };

  return (
    <>
      <h1>GitHub GraphQL API で statusを更新</h1>
      <form onSubmit={handleSubmit}>
        <textarea onChange={e => setMessage(e.target.value)} cols={50} rows={2} placeholder="your status message..." required />
        <input type="text" onChange={e => setEmoji(e.target.value)} placeholder="your status emoji..." required />
        <button>更新する</button>
      </form>
      {data && <p>{`ステータスメッセージを「${data["data"]["changeUserStatus"]["status"]["message"]}」、ステータスの絵文字を「${data["data"]["changeUserStatus"]["status"]["emoji"]}」に更新しました。`}</p>}
    </>
  )
}
```

`App.tsx`と`index.html`は上と同じです。

![qiita_graphql.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1504894/37510fb9-e664-e00c-ee94-23e2c194ef62.gif)


# まとめ
最終的なディレクトリの構成はこんな感じになりました。

```terminal:ターミナル
$ tree -I "node_modules|README.md|.gitignore" -L 2
.
├── babel.config.js
├── dist
│   ├── bundle.js
│   └── bundle.js.LICENSE.txt
├── package-lock.json
├── package.json
├── src
│   ├── App.tsx
│   ├── graphql.tsx
│   ├── index.html
│   └── rest.tsx
├── tsconfig.json
└── webpack.config.js
```

ちなみに、、

```tsx:Api.tsx
<textarea onChange={e => setBio(e.target.value)} ...以下省略 />
```

このコードを見ると、`textarea`に入力されるたびにコンポーネントが再描画されると思うのですが、
Reactの本を読んでいたら、Reactの内部には重複した描画要求を調停する仕組みがあるため、
不要な再描画がされる心配はないと書いてありました。ただ、関数自体は入力のたびに呼び出されるから、
この描画関数内で重い処理を実行するのは避けるべきだそうです。

自分の備忘録ですが、誰かのためになれば幸いです。