未来の構成を先取りする ~ React サーバーコンポーネント × GraphQL ~

[GraphQLアドベントカレンダー](https://qiita.com/advent-calendar/2021/graphql)24日目の投稿です！

:santa: メリークリスマス！ :christmas_tree: 

と心の中で思いつつ、記事を書いたので見てください

## はじめに

[Next.js Conf 2021](https://nextjs.org/conf)と[React Conf 2021](https://conf.reactjs.org/) 皆さんはご覧になりましたか？

とてもワクワクする未来の話が多かったように思います。
とりわけ、僕がワクワクしたのはサーバーコンポーネントです。

コンポーネントごとに、サーバー側でデータフェッチを行い、サーバー側でレンダリングを行う。
端末に依存されず、高速にレスポンスを返すことができる世界線を少し早く体験したいなーと思い、今回のテーマを選びました。


### この記事で何をするか

サーバーコンポーネントからGraphQLでデータをフェッチし、レンダリングをします。
GraphQLサーバーを立てるのは面倒だったので、GithubのGraphQLを使わせてもらいます

## 下準備

2021年12月24日現在、Next.jsにおいてサーバーコンポーネントはAlphaバージョンのため、少し下準備が必要です
あと、TypeScriptだとうまく動かないとどこかで聞いたので、jsでやります。

### 1. Next.jsアプリケーションの作成

```bash
yarn create next-app
```

### 2. reactのバージョンをrcへ変更

```bash
yarn add react@rc react-dom@rc
```

### 3. 実験モードをONにする

```js:next.config.js
module.exports = {
 ...
  experimental: {
    concurrentFeatures: true,
    serverComponents: true,
  },
}
```

:::note
concurrentFeatures
レンダリングをサスペンスしたコンポーネントに対し、
Promiseをthrowすることで通信が終わったことを伝える機能です。（後で使います）
:::

## 実装1: サーバーコンポーネントでGraphQLがフェッチできるようにする

GithubからGraphQLでフェッチできるようにする関数を作っておきます
GraphQLは、実際はただのPOSTなので、そんなに難しくなくできます。

```js:libs/fetchGraphQLFromGithub.js
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()
}
```

:::note
裏話
本当はReact Relayを用いてやれないかなぁと画策していたのですが、
エラーのオンパレードでできなかったので、おとなしくサーバーコンポーネント対応済みのfetchを使いました
:::


## 実装2. Pageコンポーネントをサーバーコンポーネントに変更

サーバー側でレンダリングするコンポーネントは`*.server.js`という形にする必要があります
IndexPageをこの拡張子に変更し、同じくサーバーコンポーネントであるViewerコンポーネントを呼び出します

```jsx:pages/index.server.js
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の通信を行い、コンポーネントをレンダリングします。

```jsx:components/viewer.server.js
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するようにしています。

```js:libs/use-data.js
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]()
}
```

:::note
エラー以外をthrowするというのが気持ち悪いような気がしたのですが、
非同期処理を隠蔽化するためにpromiseをthrowし、Suspenseでそれを解釈するというのはいかにもReactっぽいなぁと思いました
:::

## まとめ

サーバーコンポーネント と GraphQLを用いて未来を先取りしてみました。
サーバーコンポーネントはまだまだα状態なので、hooksも使えず、APIクライアントなども限られるため、かなり不自由な世界です。

ただ、コンポーネントごとにAPIリクエストを送り、クライアント側に大きな仕事をさせない、というのはかなり未来が明るいなぁと思いました。