react-apollo と next.js を使った最速 GraphQLアプリケーション(+モックサーバー)実装

react-apollo を試しながらガチャガチャやっていると、モックサーバーなどを含め、なかなか体験が良かったので、紹介します。

こっちも参照 [世のフロントエンドエンジニアにApollo Clientを布教したい - Qiita](https://qiita.com/seya/items/26c8a0dc549a10efcdf8)

今回書くコードの動いてる例は https://github.com/mizchi-sandbox/graphql-playground を参考にしてください。 git clone して yarn install; yarn start で動くはず。

## ゴール

最終的にこんな感じのクライアント実装(next.js)が動くようになります。

```js:pages/index.js
import React from "react";
import { Query } from "react-apollo";
import gql from "graphql-tag";

const GET_USER = gql`
  {
    user {
      id
      name
    }
  }
`;

export default () => {
  return (
    <Query query={GET_USER} ssr={true}>
      {props => {
        if (props.loading) {
          return "Loading...";
        }
        return `Hello, ${props.data.user.name} - ${props.data.user.id}`;
      }}
    </Query>
  );
};
```

`user` クエリの実装はスキーマに従っているのですが、このレスポンスはモックから返却しています。
GraphQLサーバを最初に全部実装しなくとも、スキーマ定義からそれっぽい挙動を返却して開発を始めることが出来ます。


## サーバーを実装する

まずGraphQLのスキーマを書きます。今回はGraphQLそのものの説明はしないので、最小限のスキーマ定義にします。

```:schema.graphql
type User {
  id: ID!
  name: String!
}

type Query {
  user: User!
}
```

クエリ `query { user { name } }` 叩いたら user.name に文字列が入ってる、ぐらいの実装です。

このスキーマに従う node の graphql サーバーをさっくり実装しましょう。テスト用なのと、このコードはモックサーバーを作る際に使い回すので、最終的に別言語の実装になっても大丈夫です。

```
yarn add express body-parser apollo-graphql-express graphql-tools
```

あたりをやっておいてください。足りなかったら適宜追加で。
サーバー側に babel を仕込むのが面倒だったので、今回サーバーは commonjs, クライアントは ESM の import で書いています。

```js:server.js
const path = require("path");
const fs = require("fs");
const express = require("express");
const bodyParser = require("body-parser");
const { graphqlExpress, graphiqlExpress } = require("apollo-server-express");
const {
  makeExecutableSchema,
  addMockFunctionsToSchema
} = require("graphql-tools");
const cors = require("cors");

const resolvers = {
  Query: {
    user() {
      return { name: "hoge", id: "hoge" }
    }
  }
}

const schema = makeExecutableSchema({
  typeDefs: fs
    .readFileSync(path.join(__dirname, "schema.graphql"))
    .toString(),
  resolvers
});

const app = express();
app.use(cors());
app.use("/graphql", bodyParser.json(), graphqlExpress({ schema }));
app.use("/graphiql", graphiqlExpress({ endpointURL: "/graphql" }));
app.listen(3001);
```

最小といいつつ cors 対応や graphiql 対応もしてますがデバッグで使うと思うので実装してます。
気にするべきは、 `resolvers` がサーバーとしての実装の本体です。`type User` を満たす値もしくは `Promise<User>` を返却すれば良いです。スキーマに違反していると例外を発生させます。

これを `node server.js` で実行すると http://localhost:3001/graphql に GraphQL のエンドポイントが出来ます。

ここで http://localhost:3001/graphiql を開くと、 graphiql という対話型のプレイグラウンドでクエリを実行できます

![](https://gyazo.com/32b5f2bfd9315a6888a8326e972be7b9.png)

最終的にはここでtoken確認したりして認証をかけたりするかもしれません。

## GraphQL クライアントを実装する

今回はビルド環境などをサボるため、さっくり next で動かします。

```
yarn add next react react-dom react-apollo isomorphic-fetch apollo-boost
```

まず ReactApollo を叩く準備として、すべての画面のルート要素となる `pages/_app.js` に ApolloProvider を仕込みます。このファイル名は、 next.js の規約です。

```js:pages/_app.js
import React from "react";
import { Container } from "next/app";
import { ApolloClient } from "apollo-boost";
import { HttpLink } from "apollo-boost";
import { InMemoryCache } from "apollo-boost";
import fetch from "isomorphic-unfetch";
import { ApolloProvider } from "react-apollo";

const IS_BROWSER = !!process.browser;

if (!IS_BROWSER) {
  global.fetch = fetch;
}

const URI_ENDPOINT = "http://localhost:3001/graphql";
function createClient(initialState) {
  return new ApolloClient({
    connectToDevTools: IS_BROWSER,
    ssrMode: !IS_BROWSER, // Disables forceFetch on the server (so queries are only run once)
    link: new HttpLink({
      uri: URI_ENDPOINT, // Server URL (must be absolute)
      credentials: "same-origin" // Additional fetch() options like `credentials` or `headers`
    }),
    cache: new InMemoryCache().restore(initialState || {})
  });
}

const client = createClient();

export default props => {
  const { Component, pageProps, apolloClient } = props;
  return (
    <Container>
      <ApolloProvider client={client}>
        <Component {...pageProps} />
      </ApolloProvider>
    </Container>
  );
};
```

ここで大事なのは、 HttpLink で http://localhost:3001/graphql に向けてることです。ここを切り替えることで異なる通信でのアダプタに接続するわけですね。後で使います。

他にも、SSR の挙動のために process.browser を見て(next.jsの機能) 挙動を切り替えています。とはいえ、これは動作確認用のサボってる実装で、ちゃんとSSRやりたい人は https://github.com/zeit/next.js/tree/master/examples/with-apollo を参考にしてください。

次に、`/` を見たときに返す React の Component を実装します。

```js:pages/index.js
import React from "react";
import { Query } from "react-apollo";
import gql from "graphql-tag";

const GET_USER = gql`
  {
    user {
      id
      name
    }
  }
`;

export default () => {
  return (
    <Query query={GET_USER} ssr={true}>
      {props => {
        if (props.loading) {
          return "Loading...";
        }
        return `Hello, ${props.data.user.name} - ${props.data.user.id}`;
      }}
    </Query>
  );
};
```

ReactApollo の `<Query query=...>`  のComponent を使うと、renderProp スタイルでクエリの結果が渡されてくるので、これを使って画面を描画します。最近追加された機能ですが、これがすごく便利。

ここまで実装したら、`yarn next` で localhost:3000 でクライアントを起動。
ブラウザで http://localhost:3000 を開き、確認します。

## モックサーバーを実装する

```
yarn add apollo-cache-inmemory apollo-link-schema
```

graphql サーバーを次のように `addMockFunctionsToSchema` でモック実装の挙動に切り替えます

```js:server.js
// ...
const mocks = {
  ID: () => "<id>",
  String: () => "<string>",
  User: () => ({
    id: () => Date.now().toString(),
    name: () => "user_by_type"
  }),
  Query: () => ({
    user: () => ({
      id: () => Date.now().toString(),
      name: () => "user_by_query"
    })
  })
};

const {
  makeExecutableSchema,
  addMockFunctionsToSchema
} = require("graphql-tools");

const schema = makeExecutableSchema({
  typeDefs: fs
    .readFileSync(path.join(__dirname, "../schema.graphql"))
    .toString()
});

addMockFunctionsToSchema({ schema, mocks });
// ...
```

見るべきところは `mocks` です。型に対応するモックデータを書いてやると、それを返却するようになります。試しに Query を消してみたりしてください。User の方のモックデータが返却されるはずで、それも消すと、ID や String や Int の型のデフォルトでモックされてるデータが変えるはずです。

Query と Mutation は 特殊な type で、ここに書かれたものが特に名前空間を指定しない場合のクエリ名になります。


これで `node server.js` でgraphql サーバーを起動し直すと、モックデータ側の処理に切り替わるはずです。

## クライアントで完結して mock を差し込む

ここまで来ると、Storybook みたいなコンポーネントカタログやテスト環境では、サーバーの存在に依存せず、定義とモック実装だけ借りてクライアントで完結したいですよね。しましょう。

まずクライアントでMock用の Provider を作ります。

```js:MockProvider.js
import React from "react";
import { makeExecutableSchema, addMockFunctionsToSchema } from "graphql-tools";
import { ApolloClient } from "apollo-client";
import { ApolloProvider, Query } from "react-apollo";
import { InMemoryCache } from "apollo-cache-inmemory";
import { SchemaLink } from "apollo-link-schema";

const typedefs = `
type User {
  id: ID!
  name: String!
}

type Query {
  user: User!
}
`


const mocks = {
  User: () => ({
    id: () => Date.now().toString(),
    name: () => "user_by_type"
  }),
  Query: () => ({
    user: () => ({
      id: () => Date.now().toString(),
      name: () => "user_by_query"
    })
  })
}

const schema = makeExecutableSchema({ typeDefs });

addMockFunctionsToSchema({
  schema,
  mocks
});

const client = new ApolloClient({
  ssrMode: true,
  link: new SchemaLink({ schema }),
  cache: new InMemoryCache()
});

export default ({ children }) => (
  <ApolloProvider client={client}>{children}</ApolloProvider>
);
```

今までサーバーでやっていた graphql の makeExecutableSchama をクライアントで実装し、apolloClient の link も HttpLink から SchemaLink に変えています。これによって、クライアント側で GraphQL を実行するようになるわけです。

これを使って storybook から先の pages/index.js を実行してみましょう。(storybook の導入は略)

```js:stories/pages_index.stories.js
import React from "react";
import { storiesOf } from "@storybook/react";
import MockProvider from "../MockProvider";
import Index from "../pages/index";

storiesOf("Mock GraphQL example", module).add("Mocked Index", () => (
  <MockProvider>
    <Index />
  </MockProvider>
));
```

これで上記のモック定義に従ってサーバーを立てずとも動きます。

(apollo-react/test-utils に MockedProvider というモジュールがあったんですが、試した限りは動きませんでした)

今回は node でサーバーを実装しましたが、スキーマ定義さえ共有してれば別言語の実装に変えたり、graphcool のようなPaaSを使ったり、prisma のようなGraphQL実装を使ってもいいはずです。

## 実際の開発フロー

- graphql サーバーを mock モードでそれらしいデータを返却
- graphiql の localhost:3001/graphiql などで挙動を確認
- クライアントから ApolloProvider で任意の実装に接続
- GraphQLサーバーを任意の言語で実装
- 疎通確認

こんな感じになると思います。副作用起こさなければ一通り問題ないはず。