ReScript x Next.jsの環境にGraphQLのライブラリ"Relay"を導入した

[Next.jsでReScript, tailwindを使う](https://qiita.com/Nagatatz/items/1596d86df337c06e3812)
の環境を元に、`GraphQL`のkickを試みる

# GraphQL

Facebookにより開発されたクエリ言語。
クライアント側でレスポンス形式を指定できる。

`React`で`GraphQL`のクライアントを選択するとき、大きいのは`Apollo Client`, `Relay`の2つのようだ。
[GraphQLのサイト](https://graphql.org/code/#javascript)にサーバ側や他のクライアントも載っている。

## Apollo Client

[Apollo Client](https://www.apollographql.com/docs/react/)
[Rescript Apollo Client](https://reasonml-community.github.io/rescript-apollo-client/docs/)

## Relay

[Relay](https://relay.dev/)
[RescriptRelay](https://rescript-relay-documentation.vercel.app/)

# 実装

基本的に[`RescriptRelay`の説明](https://rescript-relay-documentation.vercel.app/docs/getting-started)通りに進めていく。

## relay追加

[`Suspense`を利用](https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser)するため`react`ライブラリを`experimental`にする。

```bash
yarn add react@0.0.0-experimental-4e08fb10c react-dom@0.0.0-experimental-4e08fb10c
yarn add relay-runtime@11.0.0 relay-compiler@11.0.0 react-relay@11.0.0 relay-config@11.0.0
yarn add --dev rescript-relay graphql reason-promise bs-fetch
```

## relay設定

### config

```javascript:relay.config.js
module.exports = {
    src: "./src",
    schema: "./schema.graphql",
    artifactDirectory: "./src/__generated__",
    customScalars: {
        Datetime: "string",
        Color: "Color.t",
    },
};
```

`RescriptRelay`の`config`の`src`ディレクトリ指定方法の影響で`Next.js`の`res`ファイルを一つのフォルダに統合するために`src`に移す

```bash
mv pages components bindings src/ 
```

```diff_json:bsconfig.json
  "sources": [
-    {
-      "dir": "components",
-      "subdirs": true
-    },
-    {
-      "dir": "pages",
-      "subdirs": true
-    },
-    {
-      "dir": "bindings",
-      "subdirs": true
-    }
+    {
+      "dir": "src",
+      "subdirs": true
+    }
  ],
+  "ppx-flags": [
+    "rescript-relay/ppx"
+  ],
  "bs-dependencies": [
-    "@rescript/react"
+    "@rescript/react",
+    "rescript-relay",
+    "reason-promise",
+    "bs-fetch"
  ],
```

`tailwind`を利用している場合もディレクトリを変更する

```diff_javascript:tailwind.config.js
module.exports = {
    purge: [
-        './pages/**/*.res',
-        './components/**/*.res',
+        './src/pages/**/*.res',
+        './src/components/**/*.res',
    ],
    darkMode: false, // or 'media' or 'class'
    theme: {
        extend: {},
    },
    variants: {
        extend: {},
    },
    plugins: [],
}
```

```diff_json:package.json
  "scripts": {
    "dev": "concurrently \"bsb -clean-world -make-world -w\" \"next dev\"",
    "dev:reason": "bsb -clean-world -make-world -w",
    "dev:next": "next dev",
    "build": "bsb -clean-world -make-world && next build",
-    "start": "next start"
+    "start": "next start",
+    "relay": "rescript-relay-compiler",
+    "relay:watch": "rescript-relay-compiler --watch"
  },
```

```diff_javascript:next.config.js
const withTM = require('next-transpile-modules')(['bs-platform'])

module.exports = withTM({
-    pageExtensions: ['jsx', 'js', 'bs.js']
+    pageExtensions: ['jsx', 'js', 'bs.js'],
+    experimental: {
+        reactMode: 'concurrent'
+    }
})
```

experimental設定を有効にする。
忘れると`Cannot hydrate Suspense in legacy mode`エラーが出るので注意。

### es6 -> commonjsに変更


bs-fetchの関係で`bsconfig.json`の`package-specs`を`es6`から`commonjs`に変更する。

[Next.jsをReasonMLで用いてページ遷移しWeb APIを叩きレスポンスを表示する - bs-fetch, bs-jsonの追加](https://qiita.com/Nagatatz/items/149b53caad9b33447637#bs-fetch-bs-json%E3%81%AE%E8%BF%BD%E5%8A%A0)を参考にする。

```diff_json:bsconfig.json
  "package-specs": {
-    "module": "es6",
+    "module": "commonjs",
    "in-source": true
  },
```

### config component書く

```rescript:src/util/RelayEnv.res
/* This is just a custom exception to indicate that something went wrong. */
exception Graphql_error(string)

/**
 * A standard fetch that sends our operation and variables to the
 * GraphQL server, and then decodes and returns the response.
 */
let fetchQuery: RescriptRelay.Network.fetchFunctionPromise = (
  operation,
  variables,
  _cacheConfig,
  _uploadables,
) => {
    open Fetch
    fetchWithInit(
        Env.graphQlUrl,
        RequestInit.make(
            ~method_=Post,
            ~body=Js.Dict.fromList(list{
                ("query", Js.Json.string(operation.text)),
                ("variables", variables),
            })
            ->Js.Json.object_
            ->Js.Json.stringify
            ->BodyInit.make,
            ~headers=HeadersInit.make({
                "content-type": "application/json",
                "accept": "application/json",
//                "authorization": "Bearer " ++ Env.graphQlToken,  // 今回は認可（authorization）を要しないのでこの行は不要。graphCMS等用いる場合記述する。
            }),
            (),
        ),
    ) |> Js.Promise.then_(resp =>
        if Response.ok(resp) {
            Response.json(resp)
        } else {
            Js.Promise.reject(Graphql_error("Request failed: " ++ Response.statusText(resp)))
        }
    )
}

let network = RescriptRelay.Network.makePromiseBased(~fetchFunction=fetchQuery, ())

let environment = RescriptRelay.Environment.make(
        ~network,
        ~store=RescriptRelay.Store.make(
        ~source=RescriptRelay.RecordSource.make(),
        ~gcReleaseBufferSize=10, /* This sets the query cache size to 10 */
        (),
    ),
    (),
)
```

```rescript:src/util/Env.res
// api
@val external graphQlUrl:string = "process.env.NEXT_PUBLIC_GRAPHQL_URL"
// @val external graphQlToken:string = "process.env.NEXT_PUBLIC_GRAPHQL_TOKEN"  // authorizationが必要な場合
```

Next.jsにてブラウザでも環境変数を用いる場合、[冒頭に`NEXT_PUBLIC_`を冠する必要がある](https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser)

```:.env.local
NEXT_PUBLIC_GRAPHQL_URL="<YOUR_GRAPHQL_API_URL>"
# NEXT_PUBLIC_GRAPHQL_TOKEN="<YOUR_GRAPHQL_API_TOKEN>"  # authorizationが必要な場合
```

今回は[SpaceX API](https://api.spacex.land/graphql/)を用いる。
以下のように書き換える。

```:.env.local
NEXT_PUBLIC_GRAPHQL_URL=https://api.spacex.land/graphql/
```

### schema download

APIからschemaをダウンロードする。

```bash
yarn add --dev get-graphql-schema
yarn get-graphql-schema https://api.spacex.land/graphql/ > schema.graphql
```

## Provider

```rescript:src/components/App.res
@react.component
let make = (~children) => <RescriptRelay.Context.Provider environment=RelayEnv.environment>
    <div>
        children
    </div>
</RescriptRelay.Context.Provider>

let default = make
```

```javascript:src/pages/_app.js
import '../../styles/main.css'
import App from '../components/App.bs'

const MyApp = ({ Component, pageProps }) => (<App>
        <Component {...pageProps} />
</App>);

export default MyApp;
```

## GrapghQLたたくコンポーネント

```rescript:src/components/FetchMissions.res
module MyQuery = %relay(
    `query FetchMissionsQuery {
        missions {
             name
             id
             description
        }
    }`
)

@react.component
let make = () => {
    let response = MyQuery.use(~variables=(), ())

    let missions = switch (response.missions) {
       | Some(data) => data
       | _ => []
    }

    <div>
        <div><p className="font-black">{React.string("missions")}</p></div>
        {
            missions
            ->Belt.Array.map(
                mission => switch(mission) {
                    | Some(data) =>  <div key={Belt.Option.getWithDefault(data.id, "XXXXX")}>
                            <div><p className="font-medium">{React.string(Belt.Option.getWithDefault(data.name, ""))}</p></div>
                            <div>{React.string(Belt.Option.getWithDefault(data.description, ""))}</div>
                            <hr/>
                        </div>
                    | _ => <></>
                }
            )->React.array
        }
    </div>
}

let default = make
```

`query FetchMissionsQuery`の箇所は、ファイル名 + Queryの規則でつけなければいけないことに注意。Query以外もFetch等のパターンがある（が割愛）
queryの組立はGraphQLのURLをブラウザで開いた時にクエリを試してそのレスポンスを確認することができる。
![ＧｒａｐｈＱＬ Page](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/4fb34c62-41aa-528c-070d-59870f2fb53a.png)

レスポンスの型については、ダウンロードした`schema.graphql`を元に`realy:watch`で作成される`.res`ファイルに記載される。

```rescript:FetchMissionsQuery_graphql.res
/* @generated */
%%raw("/* @generated */")
module Types = {
  @@ocaml.warning("-30")
  
  type rec response_missions = {
    name: option<string>,
    id: option<string>,
    description: option<string>,
  }
  type response = {
    missions: option<array<option<response_missions>>>,
  }
  type rawResponse = response
  type variables = unit
}
```

`id`が`option`なのはなんでなんですかね。イテレーションを用いる場合、コンポーネントに固有の`key`を振ることになっているが、`id`ないやつ複数あると怒られるはず…
`option`あると剥がすのめんどくさいので、なんかもうちょっとどうにかして欲しかった感


```rescript:src/page/missions.res
@react.component
let make = () => {
    <div>
        <Header />
        <div className="py-6 md:py-12">
            <div className="container px-4 mx-auto">
                <React.Suspense fallback={<div>{React.string(`Loading...`)}</div>}>
                    <FetchMissions />
                </React.Suspense>
            </div>
        </div>
    </div>
}

let default = make
```

Relayを使ったコンポーネントの外側を`React.Suspense`でくるみ、`Suspense`のタグ内でfallbackした際(まだ読み込まれてない場合、読込に失敗した場合)に表示されるコンポーネントを指定する。
外側であればよく、例えば`src/components/App.res`で`<div></div>`の外側を`Suspense`でくるんでも構わない。ただ全体が読込中扱いになるので、基本的にしないはず。

# dev実行

```bash
yarn relay:watch
```

```bash
yarn dev:rescript
```

`yarn dev:reason`から改名

```bash
yarn dev:next
```

`http://localhost:3000/missions`にアクセスする。

読込時はLoading...の文字が表示され、読込が終わると下のように表示される。(一部だけトリミング)
![アクセス結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/68992/31b3e3c1-76b0-c3e0-3d95-39d3e68fec58.png)


# 感想

クエリ名の指定に癖がある
`src`のディレクトリが束縛される
`GraphQL`、`fetch`で叩くのもありでは。
エンドポイント複数あったときはどうしよう？（あまりないケースか？)

# やったけどダメだったこと

## rescript-apollo-client

###　原因

`dependency`が`reason-react`だった。無理矢理書き換えてビルドし直すのも試したが、上手くいかなかった。

`invalid hook`のエラーが出て直せなかった

## swapi

STAR WARSのキャラ名とか返してくれるAPI群。最初これ使おうと思ってた。

[swapi-qlaphql(GitHub)](https://github.com/graphql/swapi-graphql)
[swapi-graphql testpage](https://graphql.org/swapi-graphql)

`get-graphql-schema`で`invalid-json`が返ってきた。なんでや

# 実運用

同様の構成で[graphCMS](https://graphcms.com/)を利用してます。

# 参考文献

[GraphQL](https://graphql.org/)
[GraphQL Code Libraries, Tools and Services](https://graphql.org/code/#javascript)
[Apollo](https://www.apollographql.com/)
[Apollo Client](https://www.apollographql.com/docs/react/)
[Rescript Apollo Client](https://reasonml-community.github.io/rescript-apollo-client/docs/)
[Relay](https://relay.dev/)
[RescriptRelay](https://rescript-relay-documentation.vercel.app/)
[Getting Started with RescriptRelay | RescriptRelay](https://rescript-relay-documentation.vercel.app/docs/getting-started)
[サスペンスを使ったデータ取得（実験的機能）](https://ja.reactjs.org/docs/concurrent-mode-suspense.html)
[Next.jsをReasonMLで用いてページ遷移しWeb APIを叩きレスポンスを表示する - bs-fetch, bs-jsonの追加](https://qiita.com/Nagatatz/items/149b53caad9b33447637#bs-fetch-bs-json%E3%81%AE%E8%BF%BD%E5%8A%A0)
[swapi-qlaphql(GitHub)](https://github.com/graphql/swapi-graphql)
[swapi-graphql testpage](https://graphql.org/swapi-graphql)
[SpaceX API](https://api.spacex.land/graphql/)
[Next.js - Basic Features: Environment Variables - Exposing Environment Variables to the Browser](https://nextjs.org/docs/basic-features/environment-variables#exposing-environment-variables-to-the-browser)
[graphCMS](https://graphcms.com/)