tettyはエモがわからない

Web Developerとしてフリーランスやスタートアップでモノづくり(開発)をしているtettyのぼやきが見れる場所。

全集中 Reactの呼吸 Relayの舞 『戯れ』

はじめに

こちらは『DMMグループ '20卒内定者 Advent Calendar 2019'』21日目の記事になります。

本記事は投稿主がNext(React)でGraphqlを使いたいが「apollo多いよね。relay使ってみたくない?」というお気持ちでreact-relayを用いた スターターキットを作り戯れる話 です。

GraphqlとRelayについて

Graphql

てってぃのモノづくり工房 - Graphql

TOPかっこいいですね。非常にクールです。

GraphqlはRESTの課題解決をしようぜというお気持ちから生まれた規格です。 解決したい課題としてはいくつかありますが、投稿主が使っていて感じている利点として 必要なリソースを1つのリクエストで得られるという点にあるのかなと。

おかげさまで幾度もフェッチせずに済み、必要なデータもコンポーネントごとにfragment(Graphqlクエリの断片化)を定義することでDX最高です。

Relay

てってぃのモノづくり工房 - Relay

Graphqlのクールさとは相まって、ポップなTOPが良いですね。

Relayは単一のGraphqlリクエストで結果を得られるようにしたり、Mutation(データの作成・更新)も簡易的に実行できるようサポートされているGraphqlクライアントです。ありがたい。

もちろん、Graphqlクライアントということで以下のようなメリットがあります。

  • コレクションのページネーション
  • データキャッシュ
  • Mutation後にキャッシュの一貫性保持

また、Relayには制限もあります。

  • 採用技術がReact or ReactNativeになる。
  • 導入するアプリケーション構造の自由度を下げる。
  • Relay Server Specificationの規約でクエリとレスポンスの冗長性が少し増す。

Apolloはよく聞くけど、Relayはあまり聞かないよね。

日本記事の検索("react graphql")

てってぃのモノづくり工房 - react graphql

アポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロアポロ。

海外記事の検索("how to use graphql with react")

てってぃのモノづくり工房 - how to use graphql with react

ようやく”Relay”の単語が出てきましたね!ちなみに、この記事の内容はApolloでした。

GraphqlクライアントにApolloが多い理由

単純な理由がいくつかあります。

  • Apolloは様々な環境で使用できるが、RelayはReactがメインである。
  • Apolloは軽量で柔軟なアプローチができるが、Relayは自由度が低い。
  • Apolloはとにかく資料が豊富でキャッチアップが早いが、Relayの資料は少ない。

Apolloはこれから学習するので詳細には語れませんが、細かい比較内容をそのうち記事化します。

現在ではVue.jsでもRelayを導入する個人制作のパッケージ(vue-relay)などがありますが、やはり公式としてはReactオンリーのようです。

スターターキットを作る

0. 前提

  • APIサーバにGraphqlが採用されている。

  • フロントエンド構成は以下を利用する。

    • TypeScript: ^3.7
    • Next: ^9.1
    • React
    • nodenv
    • react-relay
    • relay-runtime
    • relay-compiler
    • @graphql-codegen
    • @material-ui など

1. フロントエンド構成の説明

Next.js

今回、フロントエンドのフレームワークはZeit社のNext.jsを使用していきます。NextはReactを包含してSSRを可能とする素敵な技術ですが、仕事先のプロジェクトでがっつり使っていくということで今回はReact単体ではなくNextも含めて採用しました。

https://nextjs.org/

TypeScipt(3.7系)

Graphqlの型システムを利用するにはこれしかないでしょ!ということでいつものTypeScriptです。3.7系からOptional Chainingが利用できるという点も含めて最新を採用します。

www.typescriptlang.org

nodenv

言わずもがな、nodenv。 プロジェクトごとにnodeバージョンを切り替えます。

github.com

react-relay, relay-runtime, relay-compiler

ReactでRelayを使用するコアパッケージや、Graphqlのスキーマファイルから各コンポーネントに対応する型定義ファイルを自動生成してくれる方々です。

relay.dev

@graphql-codegen

GraphqlスキーマファイルからTSの型定義ファイルを生成することに利用します。便利。

graphql-code-generator.com

@material-ui

毎度お馴染み、Material Designのパッケージです。

material-ui.com

2. Next + TSの環境作成

ざっとこんな感じの構成を作りました。 尚、Nextはpagesディレクトリなど、Next規定のディレクトリやファイルを作成すればちゃんと認識してくれます。

次セクションでディレクトリやファイルをピックアップして解説します。

project/
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── _error.tsx
│   └── index.tsx
├── public
│   ├── fonts
│   ├── icons
│   └── images
├── src
│   ├── components
│   ├── layouts
│   ├── lib
│   ├── styles
│   └── types
│ 
└── tsconfig.json

next.config.js

Nextの設定ファイルになります。Nuxtでいうnuxt.configのようなものになります。

  • Dotenv

環境を振り分けるケースがわりかし多かったので入れました。 注意点としては通常のDotenvではなくdotenv-webpackを使用することになる点です。

  • withSass

今回はsrc/stylesにスタイリングファイル(Sass)を分離し、CSS moduleとして各コンポーネントで利用したかったので使用しました。

const withSass = require('@zeit/next-sass');
const Dotenv = require('dotenv-webpack');
const path = require('path');

module.exports = withSass({
  cssModules: true,
  sassLoaderOptions: {
    includePaths: ["src/styles/**/*.sass"]
  },
  webpack: config => {
    config.plugins = config.plugins || [];

    config.plugins = [
      ...config.plugins,
      new Dotenv({
        path: path.join(__dirname, '.env'),
        systemvars: true
      })
    ];

    return config
  }
});

pages/

Nextがページとして認識するディレクトリです。 各ページファイルだけでなく、Next規定のファイル(_hogehoge.tsx)も設置します。

public/

アセットファイルを設置するディレクトリです。 以前はstaticでしたが、最近のバージョンではpublicに名を変えています。

src/components

コンポーネントを格納します。 Graphqlのfragment(断片化されたクエリ)を定義したコンポーネントなども含めるため、relay-compilerに読み取らせると各fragmentコンポーネントに必要な型定義が生成されます。 こんな感じ。

てってぃのモノづくり工房 -flagmentコンポーネント

src/layouts

ページのレイアウトファイルを格納しています。 headerやfooterをセットにしたファイルですね。

src/lib

material-uiのテーマファイルやRelayのコンフィグ、ユーティリティなどを格納します。

src/styles

CSS moduleとして使用するSassファイルを格納します。 ここの細かい構造はまた今度。

3. Graphqlスキーマの生成

Graphqlのエンドポイントからスキーマファイルを生成するためget-graphql-schemaをインストールします。

使用

インストール後、Graphqlエンドポイントへ向けてスキーマファイルを生成するためのコマンドを実行します。

> get-graphql-schema <endpoint_url> schema.graphql

するとsrc/types/schema.graphqlが1秒もかからずにファイルが生成され、以下のようになります。

type User implements Node {
  id: ID!
  user: User!
}

"""The connection type for History."""
type UserConnection {
  """A list of edges."""
  edges: [UserEdge]

  """Information to aid in pagination."""
  pageInfo: PageInfo!
  totalCount: Int!
}
...

// スキーマが続く。すごいぞ。

4. コードジェネレーターの導入

Graphqlスキーマファイルから型定義ファイルを自動生成するため@graphql-codegenを導入します。 ひとまず、この辺りのパッケージを入れておきました。

"@graphql-codegen/cli": "^1.9.1", // 必須
"@graphql-codegen/typescript": "^1.9.1", //必須
"@graphql-codegen/typescript-operations": "^1.9.1",
"@graphql-codegen/typescript-resolvers": "^1.9.1",

また、自動生成にあたってのyamlを定義しなければならないのですが、最初は特にこだわることもないかなと思いますので以下のようにシンプルな定義にします。

overwrite: true
schema: "schema.graphql"
generates:
  src/types/graphql.ts:
    plugins:
      - typescript
      - typescript-resolvers

使用

諸々が整った段階でコマンドを実行します。いざ、ジェネレート。

> graphql-codegen --config codegen.yml

するとスキーマからsrc/types/graphql.tsが生成され、スキーマの型定義ファイルとして使用できるようになります。このような感じ。openAPIと同様ですね。

import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
export type Maybe<T> = T | null;
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type RequireFields<T, K extends keyof T> = { [X in Exclude<keyof T, K>]?: T[X] } & { [P in K]-?: NonNullable<T[P]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string,
  String: string,
  Boolean: boolean,
  Int: number,
  Float: number,
  /** An ISO 8601-encoded datetime */
  ISO8601DateTime: any,
};

...

//型定義が続く。強いぞジェネレータ。

5. RelayのEnvironmentファイル作成

クライアント-Graphqlエンドポイント間での通信やレスポンスをキャッシュするためのストアなど、Relayをフルに使っていくために定義をする必要があります。

import {Environment, Network, RecordSource, Store} from 'relay-runtime';

export const fetchQuery = (operation: any, variables: any) => {
  return fetch(process.env.RELAY_ENDPOINT as string, {
    method: 'POST',
    body: JSON.stringify({
      query: operation.text,
      variables,
    }),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
  }).then((response: Response) => {
    if (response.ok) {
      return Promise.reject(response.statusText);
    }
    return response.json()
  });
};

const environment = new Environment({
  network: Network.create(fetchQuery),
  store: new Store(new RecordSource())
});

export default environment;

ここで重要なのが以下の2つ。

  • NetWork

  • Store

    • レスポンスを格納しキャッシュ、ローカルステートとして利用できるようにしてくれる。

6. Envファイル作成と完成

最後に、エンドポイントは様々なのでenvファイルで対応しておきましょう。

RELAY_ENDPOINT="http://localhost:xxxx/graphql"

以上でキットが完成しました。 そして、このキットを用いることでGraphql + Reactの開発を開始することができます。やったね。

感想

環境構築から既にしんどい思いをしたので、プロジェクトの立ち上がりがそこそこ遅いです。ですので、やるならばしっかりとスターターキットを作るべきだと感じました。

問題は土台のメンテナンスコストが負担になる点でしょうか。 環境構築の時点で非常に多くのパッケージを入れたかと思います。

また、Apolloも使ってみてどちらを採用するのか検討していきたいと思いますが、その前にせっかくRelayのキットを作ったので、 次回はキットを用いてコンポーネントを実装していく記事を書いていきたいと思います。

ではまた。

参考リポジトリ

本日のスターターはこちら。 まだ作っただけですので、READMEやexampleなど整備していきます。

github.com  

その他

Githubが公式でGraphqlのお試しを用意しているので、触ってみたい方はぜひ。 Docsをクリックすると叩けるクエリなどが表示されます。(これ、なんと自分らのクライアントでもできてしまうんです。)

developer.github.com

あ、クエリの叩き方はご自分で勉強してくださいね。

graphql.org