GraphQLアドベントカレンダー24日目の投稿です!
メリークリスマス!
と心の中で思いつつ、記事を書いたので見てください
はじめに
Next.js Conf 2021とReact Conf 2021 皆さんはご覧になりましたか?
とてもワクワクする未来の話が多かったように思います。
とりわけ、僕がワクワクしたのはサーバーコンポーネントです。
コンポーネントごとに、サーバー側でデータフェッチを行い、サーバー側でレンダリングを行う。
端末に依存されず、高速にレスポンスを返すことができる世界線を少し早く体験したいなーと思い、今回のテーマを選びました。
この記事で何をするか
サーバーコンポーネントからGraphQLでデータをフェッチし、レンダリングをします。
GraphQLサーバーを立てるのは面倒だったので、GithubのGraphQLを使わせてもらいます
下準備
2021年12月24日現在、Next.jsにおいてサーバーコンポーネントはAlphaバージョンのため、少し下準備が必要です
あと、TypeScriptだとうまく動かないとどこかで聞いたので、jsでやります。
1. Next.jsアプリケーションの作成
yarn create next-app
2. reactのバージョンをrcへ変更
yarn add react@rc react-dom@rc
3. 実験モードをONにする
module.exports = {
...
experimental: {
concurrentFeatures: true,
serverComponents: true,
},
}
concurrentFeatures
レンダリングをサスペンスしたコンポーネントに対し、
Promiseをthrowすることで通信が終わったことを伝える機能です。(後で使います)
実装1: サーバーコンポーネントでGraphQLがフェッチできるようにする
GithubからGraphQLでフェッチできるようにする関数を作っておきます
GraphQLは、実際はただのPOSTなので、そんなに難しくなくできます。
export const fetchGraphQLFromGithub = async (query, variables) => {
return (
await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
Authorization: `bearer ${process.env.GRAPHQL_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query,
variables,
}),
})
).json()
}
裏話
本当はReact Relayを用いてやれないかなぁと画策していたのですが、
エラーのオンパレードでできなかったので、おとなしくサーバーコンポーネント対応済みのfetchを使いました
実装2. Pageコンポーネントをサーバーコンポーネントに変更
サーバー側でレンダリングするコンポーネントは*.server.js
という形にする必要があります
IndexPageをこの拡張子に変更し、同じくサーバーコンポーネントであるViewerコンポーネントを呼び出します
import Viewer from '../components/viewer.server'
export default function Index() {
return (
<div>
<h1>Server Components with GraphQL Example</h1>
<Viewer />
</div>
)
}
実装3. Viewerコンポーネントを作成
Viewerコンポーネントでは、GraphQLの通信を行い、コンポーネントをレンダリングします。
import { Suspense } from 'react'
import { useData } from '../libs/use-data'
import { fetchGraphQLFromGithub } from '../libs/fetch-from-github'
export default function Viewer() {
const query = `
query {
viewer {
login
}
}
`
const data = useData('viewer', () => fetchGraphQLFromGithub(query, {}))
return <Suspense fallback={'Loading...'}>{JSON.stringify(data)}</Suspense>
}
useDataでは、Suspenseにデータフェッチが終わったことを伝えるため、promiseをthrowするようにしています。
const cache = {}
export const useData = (key, fetcher) => {
if (!cache[key]) {
let data
let promise
cache[key] = () => {
if (data !== undefined) return data
if (!promise) promise = fetcher().then((r) => (data = r))
throw promise
}
}
return cache[key]()
}
エラー以外をthrowするというのが気持ち悪いような気がしたのですが、
非同期処理を隠蔽化するためにpromiseをthrowし、Suspenseでそれを解釈するというのはいかにもReactっぽいなぁと思いました
まとめ
サーバーコンポーネント と GraphQLを用いて未来を先取りしてみました。
サーバーコンポーネントはまだまだα状態なので、hooksも使えず、APIクライアントなども限られるため、かなり不自由な世界です。
ただ、コンポーネントごとにAPIリクエストを送り、クライアント側に大きな仕事をさせない、というのはかなり未来が明るいなぁと思いました。