LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-03-28

Next.jsでReScript, tailwindを使う
の環境を元に、GraphQLのkickを試みる

GraphQL

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

ReactGraphQLのクライアントを選択するとき、大きいのはApollo Client, Relayの2つのようだ。
GraphQLのサイトにサーバ側や他のクライアントも載っている。

Apollo Client

Apollo Client
Rescript Apollo Client

Relay

Relay
RescriptRelay

実装

基本的にRescriptRelayの説明通りに進めていく。

relay追加

Suspenseを利用するためreactライブラリをexperimentalにする。

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

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

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

mv pages components bindings src/ 
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を利用している場合もディレクトリを変更する

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: [],
}
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"
  },
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.jsonpackage-specses6からcommonjsに変更する。

Next.jsをReasonMLで用いてページ遷移しWeb APIを叩きレスポンスを表示する - bs-fetch, bs-jsonの追加を参考にする。

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

config component書く

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 */
        (),
    ),
    (),
)
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_を冠する必要がある

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

今回はSpaceX APIを用いる。
以下のように書き換える。

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

schema download

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

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

Provider

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

let default = make
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たたくコンポーネント

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をブラウザで開いた時にクエリを試してそのレスポンスを確認することができる。
GraphQL Page

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

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
}

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

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実行

yarn relay:watch
yarn dev:rescript

yarn dev:reasonから改名

yarn dev:next

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

読込時はLoading...の文字が表示され、読込が終わると下のように表示される。(一部だけトリミング)
アクセス結果

感想

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

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

rescript-apollo-client

 原因

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

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

swapi

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

swapi-qlaphql(GitHub)
swapi-graphql testpage

get-graphql-schemainvalid-jsonが返ってきた。なんでや

実運用

同様の構成でgraphCMSを利用してます。

参考文献

GraphQL
GraphQL Code Libraries, Tools and Services
Apollo
Apollo Client
Rescript Apollo Client
Relay
RescriptRelay
Getting Started with RescriptRelay | RescriptRelay
サスペンスを使ったデータ取得(実験的機能)
Next.jsをReasonMLで用いてページ遷移しWeb APIを叩きレスポンスを表示する - bs-fetch, bs-jsonの追加
swapi-qlaphql(GitHub)
swapi-graphql testpage
SpaceX API
Next.js - Basic Features: Environment Variables - Exposing Environment Variables to the Browser
graphCMS

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2