Next.jsでReScript, tailwindを使う
の環境を元に、GraphQL
のkickを試みる
GraphQL
Facebookにより開発されたクエリ言語。
クライアント側でレスポンス形式を指定できる。
React
でGraphQL
のクライアントを選択するとき、大きいのはApollo Client
, Relay
の2つのようだ。
GraphQLのサイトにサーバ側や他のクライアントも載っている。
Apollo Client
Apollo Client
Rescript Apollo Client
Relay
実装
基本的に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
module.exports = {
src: "./src",
schema: "./schema.graphql",
artifactDirectory: "./src/__generated__",
customScalars: {
Datetime: "string",
Color: "Color.t",
},
};
RescriptRelay
のconfig
のsrc
ディレクトリ指定方法の影響でNext.js
のres
ファイルを一つのフォルダに統合するためにsrc
に移す
mv pages components bindings src/
"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
を利用している場合もディレクトリを変更する
module.exports = {
purge: [
- './pages/**/*.res',
- './components/**/*.res',
+ './src/pages/**/*.res',
+ './src/components/**/*.res',
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
}
"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"
},
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の追加を参考にする。
"package-specs": {
- "module": "es6",
+ "module": "commonjs",
"in-source": true
},
config component書く
/* 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 */
(),
),
(),
)
// 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_
を冠する必要がある
NEXT_PUBLIC_GRAPHQL_URL="<YOUR_GRAPHQL_API_URL>"
# NEXT_PUBLIC_GRAPHQL_TOKEN="<YOUR_GRAPHQL_API_TOKEN>" # authorizationが必要な場合
今回はSpaceX APIを用いる。
以下のように書き換える。
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
@react.component
let make = (~children) => <RescriptRelay.Context.Provider environment=RelayEnv.environment>
<div>
children
</div>
</RescriptRelay.Context.Provider>
let default = make
import '../../styles/main.css'
import App from '../components/App.bs'
const MyApp = ({ Component, pageProps }) => (<App>
<Component {...pageProps} />
</App>);
export default MyApp;
GrapghQLたたくコンポーネント
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をブラウザで開いた時にクエリを試してそのレスポンスを確認することができる。
レスポンスの型については、ダウンロードしたschema.graphql
を元にrealy:watch
で作成される.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
あると剥がすのめんどくさいので、なんかもうちょっとどうにかして欲しかった感
@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
のディレクトリが束縛される
GraphQL
、fetch
で叩くのもありでは。
エンドポイント複数あったときはどうしよう?(あまりないケースか?)
やったけどダメだったこと
rescript-apollo-client
原因
dependency
がreason-react
だった。無理矢理書き換えてビルドし直すのも試したが、上手くいかなかった。
invalid hook
のエラーが出て直せなかった
swapi
STAR WARSのキャラ名とか返してくれるAPI群。最初これ使おうと思ってた。
swapi-qlaphql(GitHub)
swapi-graphql testpage
get-graphql-schema
でinvalid-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