もふもふ技術部

IT技術系mofmofメディア

React + GraphQL + ApolloでGitHubから自分の過去のプルリクエストのタイトルを振り返ってみる

今じわじわとやりたい欲が高まっているGraphQLとReactで練習がてら何か作ってみることにする。プロジェクトでがっつり触ったことはないので入門って感じではあるが何かしら楽してありがたみを感じたい。そして動くものを触りたい。

バージョンとか

React 16.13.1 graphql 15.0.0 typescript 3.7.2

何を作るか

GraphQlのためにAPIサーバーを作るというのもそれはそれであり(やりたい)が、それをやってしまうとそれだけで終わりそうだった&とりあえずReactを触るのを優先したかったので、なんかいい感じにgraphqlのAPIを提供してくれるものはないか探してみた。 大きなサービスは結構な割合でGraphQLのAPIを提供していた。GraphQLを未来とか言ったのは誰だ とはいえ何か自分に関係するものだと馴染みやすいと思い、毎日触るであろうGitHubが提供してくれているAPIを使ってみることにした。 authorizeして自分のデータをいい感じに取得できるっぽいので、自分のPRのタイトルを出して思い出にふけってみます。

セットアップする

まずは恒例のセットアップです。

$ create-react-app github-atuh-app --typescript    

create-react-appでReactのアプリの雛形を作成します。そのままだとjsになってしまうので--typescriptオプションをつけてtypescreptにしていきます。

構築が終わったら一応確認しておきましょう。 ログの最後に

We suggest that you begin by typing:
cd github-atuh-app
yarn start

と言っているので、従っておきます。 react01 Reactのロゴがグルグルしてますね。この画面が出れば一旦はOKです。

不要なものを整理しておく

実際にコードを見ていきます。今回はReactのロゴをくるくるさせるのがゴールではないので、不要なCSSや画像などは整理していきます。 App.tsxの中身はこんな感じ。

src/App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

今回は特にデザインは当てないので、csssvgのインポートは解除しておきます。

import logo from './logo.svg';
import './App.css';

この二行は削除しましょう。

続いて、App関数でreturnされている中身も一度全て消してしまいましょう。代わりにまだ導入していませんが、hello graphglと先行で入れておきます。削った結果こうなりました。

src/App.tsx

import React from 'react';

function App() {
  return (
    <div>
      <p>Hello GraphQL</p>
    </div>
  );
}

export default App;

すっきりしましたね。 ここで一度画面がどのようになっているか確認してみます。

スクリーンショット 2020-05-20 18.00.24

このようにHello GraphQLとなっていればOKです。

GraphQLを入れる

ここまでで一旦雛形が完成したので、今回のメインの一つであるGraphQLを入れていきます。

yarn add graphql react-apollo apollo-boost dotenv  

それぞれ簡単に説明をしておくと、 - graphql graphql本体。RESTに代わる問い合わせを担ってくれます。 - react-apollo reactでapolloというgraphqlをいい感じに処理してくれるミドルウェアを使えるようにします。 - apollo-boost apolloのラッパーしたスターターパックみたいなもの。お試しで使うときなんかにno-configでいい感じに使えます。 - dotenv 環境変数をいい感じに管理してくれるやつ。 (追記) ※本来create-react-appで作成した場合デフォルトで入っているので入れなくても良よかったです

です。これらをyarnでインストールします。

GitHubトークンを入手する

先にGitHubトークンを入手し、通信を行うための準備をしましょう。

こちらのGitHubのアクセストークンを生成するから、personal access tokenを取得します。スコープはローカルで試したいだけなので一旦なしで大丈夫です。

生成が終わったら、ハッシュ値が一度だけ表示されますので、大事に持っておきましょう。後で使用します。

Clientの作成

コードに戻りましょう。最初に、GitHubAPIと連携するためのクライアントをapolloの力を借りつつ実装していきます。 src直下にとりあえずはclient.tsというファイルを作成します。

touch src/client.ts

作成したファイルに下記を記入します。

import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink } from 'apollo-link';

const GITHUB_TOKEN = process.env.REACT_APP_GITHUB_TOKEN;
const headersLink = new ApolloLink((operation, forward) => {
  operation.setContext({
    headers: {
      Authorization: `Bearer ${GITHUB_TOKEN}`
    }
  });
  return forward(operation);
});

const endpoint = 'https://api.github.com/graphql';
const httplink = new HttpLink({ uri: endpoint });
const link = ApolloLink.from([ headersLink, httplink ]);

export default new ApolloClient({
  link,
  cache: new InMemoryCache()
});

この辺は気になったら調べてやってみてねという感じなのですが、ざっくり何してるのかを解説しておくと、GitHub tokenを環境変数から取得し、ヘッダーにつけて通信の際の認証に使用できるようにしています。また、endpointは今回 'https://api.github.com/graphql’ になるので、それを設定し、最終的にヘッダーつきのリンクとendpointのリンクをいい感じに合体させたものをApolloClientのインスタンス生成時のリンクとして渡しています。これで、headerの中にトークンを含めたリクエストをhttps://api.github.com/graphq に送れるようになり、今回やりたいことに一歩近づくというわけです。

環境変数を設定する

次はtokenを読み込めるようにしていきます。 tokenはGitHubから取得したものを入れますが、ソースに直書きなどは当然ですがアウトなので、dotenvの機能を使います。Reactの環境変数の決まりごととして、環境変数には

REACT_APP_なんとかかんとか

という形で、REACT_APPというプレフィックスをつけてあげないと読めないというものがあります。今回は

const GITHUB_TOKEN = process.env.REACT_APP_GITHUB_TOKEN;

で取りたいので、REACT_APP_GITHUB_TOKENです。

.env

REACT_APP_GITHUB_TOKEN=取得したtoken

という形式で渡しましょう。ファイルでの読み取りにはprocess.envというチェーンで取ってくる事が可能です。

GraphQLを使ってアクセスしてみる

いよいよ表示です。今回はApp.tsxに全て詰め込みますが本来は分けるべきなのでそこはご容赦ください。 まずはインポートから。

import React from 'react';
import { ApolloProvider } from 'react-apollo';
import client from './client';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';

今回新しく出てきたのはgqlというものと、Queryになります。 gqlというのはgraphqlのクエリをラップする事でGraphQLで使えるような形式にパースしてくれるありがたい代物です。 もう一つのQueryというのは、react-apolloが提供するコンポーネントで、引数としてgqlでラップされたQueryとvaliablesという変数をとる事で、そのデータに応じて返す値を決める関数を内部に定義する事ができるものらしいです。

クエリを書く

GitHub GraphQL API v4 | GitHub Developer Guide 公式Docを見るといろいろできるっぽいのですが、今回はシンプルに引数にuser情報を渡して、そのname、サムネ画像、今まで作ってきたプルリクエストとそのリンクを古い順に返すようなクエリにしていこうと思います。

クエリはこのように書きました。

const USER = gql`
  query UserInfo($login: String!, $parNum: Int) {
    user(login: $login) {
      name
      avatarUrl
      pullRequests(first: $parNum) {
        nodes {
          id
          title
          permalink
        }
      }
    }
  }
`;

GraphQL自体の説明は割愛しますが、ログイン情報のGitHubIDと何件のプルリクエストのタイトルを表示させるのかを引数で定義できるような形式にしました。 ついでにせっかくTypeScriptを使っているので、返す値をinterfaceで定義しておきます。

interface pullRequestData {
  id: string;
  title: string;
  permalink: string;
}

プルリクエストの情報を次の3つの型情報として定義しておきます。

情報が出揃ったので、実際に描画する関数を書きます。

const App: React.FC = () => {
  return (
    <ApolloProvider client={client}>
      <div className="App">
        <p>Hello GraphQL</p>
        <Query query={USER} variables={{ login: 'Akito-n', parNum: 30 }}>
          {(result: any) => {
            const { loading, error, data } = result;

            if (loading) return <p>Loading...</p>;
            if (error) return <p>Error {error.message}</p>;

            return (
              <div>
                <p>{data.user.name}</p>
                <img src={data.user.avatarUrl} />
                {data.user.pullRequests.nodes.map((c: pullRequestData) => (
                  <div key={c.id}>
                    <p>{c.title}</p>
                    <a href={c.permalink}>GitHub</a>
                  </div>
                ))}
              </div>
            );
          }}
        </Query>
      </div>
    </ApolloProvider>
  );
};

export default App;

ポイントとなるのはまずここです。

 <Query query={USER} variables={{ login: 'Akito-n', parNum: 30 }}>

gql でラップしたUSERというクエリと、引数としてGitHubID(ここは自分のものに適宜置き換えてもらえるといいかと思います。),30件の情報を取得できるようにしています。そしてその中で、

{(result: any) => {
            const { loading, error, data } = result;

            if (loading) return <p>Loading...</p>;
            if (error) return <p>Error {error.message}</p>;

            return (
              <div>
                <p>{data.user.name}</p>
                <img src={data.user.avatarUrl} />
                {data.user.pullRequests.nodes.map((c: pullRequestData) => (
                  <div key={c.id}>
                    <p>{c.title}</p>
                    <a href={c.permalink}>GitHub</a>
                  </div>
                ))}
              </div>
            );
          }}

として、データの取得、エラー時の表示内容、データの表示を行っています。ApolloDocのサンプルだと

  { (result: any) =>{
  const { loading, error, data } = result;
  `
  `
  `
  略

というようなresultをany型で受け取って、内部で分割代入する方法ではなく、直接引数に{ loading, error, data } を取っていましたが、 Typescript TS2322 error with typescript 3.8.2 · Issue #3854 · apollographql/react-apollo · GitHub にあるようにTypeScreptのエラーが出てしまったので、やむなくこの形式にしています。

まとめるとこんな感じになります。 App.tsx

import React from 'react';
import { ApolloProvider } from 'react-apollo';
import client from './client';
import gql from 'graphql-tag';
import { Query } from 'react-apollo';

const USER = gql`
  query UserInfo($login: String!, $parNum: Int) {
    user(login: $login) {
      name
      avatarUrl
      pullRequests(first: $parNum) {
        nodes {
          id
          title
          permalink
        }
      }
    }
  }
`;

interface pullRequestData {
  id: string;
  title: string;
  permalink: string;
}

const App: React.FC = () => {
  return (
    <ApolloProvider client={client}>
      <div className="App">
        <p>Hello GraphQL</p>
        <Query query={USER} variables={{ login: 'Akito-n', parNum: 30 }}>
          {(result: any) => {
            const { loading, error, data } = result;

            if (loading) return <p>Loading...</p>;
            if (error) return <p>Error {error.message}</p>;

            return (
              <div>
                <p>{data.user.name}</p>
                <img src={data.user.avatarUrl} />
                {data.user.pullRequests.nodes.map((c: pullRequestData) => (
                  <div key={c.id}>
                    <p>{c.title}</p>
                    <a href={c.permalink}>GitHub</a>
                  </div>
                ))}
              </div>
            );
          }}
        </Query>
      </div>
    </ApolloProvider>
  );
};

export default App;

ではこれで一度yarn startをしてみてみましょう。

スクリーンショット 2020-05-20 23.53.33

問題なければこのようにGithubIDで入れた自分の情報が取得できました。

最初のPRは懐かしいですね。自分はいくつかGitHubアカウントを持っており、切り替えているので本当に初めてのPRというわけではないのですが、初めてのPRには懐かしさを感じますね。

※(上記の方法で進める場合、権限設定も何もしていないのでpublicなリポジトリのデータしか取れないので注意してください。)

他にも@apollo/react-hooksで使えるuseQuery useMutationとかもあってこっちの方がいいらしいので今度使ってみたいと思います。

後、機会があればファイルの分割も記事にしてみたいと思います。