Open3

Server Components で GraphQL のデータを取得するやり方を模索中

CureCure

概要

タイトルのままだが、
Server Components の await と Suspense を使って、GraphQL からデータを取得する

未消化

  • Suspense であるが、data が optional
  • graphqlClient を シングルトンにした方が良い?
  • @0no-co/graphqlsp で schema.ts を取得したいが、 backend auth で止まって、introspection が取得できない
CureCure

技術的には、以下を想定

frontend

  • Next.js with server actions
  • fetch with Session Cookie
CureCure

候補

前提として、導入 | gql.tada で gql.tada を進めていた。
ドキュメント先で urql の useQuery を使っていたが、Server Components で取得する方法がないので、調査した。

結論として、以下が良さそう。
urql の query().toPromise() メソッドを利用する
理由は、async Server Components と相性が良さそうだったため。
(もうちょっと試行錯誤が必要そう)

対策

src/app/test/page.tsx
import { Child } from '@/app/test/_components/Child/_.tsx'
import type { NextPage } from 'next'
import { Suspense } from 'react'
import type { Simplify } from 'type-fest'

type Props = Simplify<Record<string, unknown>>

const Page = (async () => {
  return (
    <main>
      Page
      <div>
        <Suspense fallback={'取得中'}>
          <Child />
        </Suspense>
      </div>
    </main>
  )
}) satisfies NextPage<Props>

// biome-ignore lint/style/noDefaultExport: App Router の仕様
export default Page
src/app/test/_components/Child/_.tsx
import { graphql } from '@/_abstract/libs/gql-tada/config/graphql'
import { graphqlClient } from '@/_abstract/libs/urql/config/client'
import type { FC } from 'react'
import type { Simplify } from 'type-fest'

type Props = Simplify<Record<string, unknown>>

const healthCheckQuery = graphql(`
  query healthCheck {
    healthCheck {
      status
    }
  }
`)

export const Child = (async () => {
  const result = await graphqlClient.query(healthCheckQuery, {}).toPromise()

  // biome-ignore lint/suspicious/noConsoleLog: TODO デバッグ後消す
  console.log(result.data?.healthCheck.status)

  return (
    <>
      tomato
      {/* HERE: 取得中 -> OK と出た */}
      {result.data?.healthCheck.status}
    </>
  )
}) satisfies FC<Props>
設定ファイル
src/_abstract/libs/urql/config/client.ts
import { serverEnv } from '@/_abstract/libs/t3-env/config/serverEnv'
import { Client, cacheExchange, fetchExchange } from '@urql/core'

/**
 * GraphQL クライアント
 *
 * TODO: data を required にしたい
 *
 * @example
 * ```ts
 * const testQuery = `
 * query testQuery($id: ID!) {
 *   user(id: $id) {
 *     id
 *     name
 *   }
 * }
 * `
 *
 * const result = await graphqlClient.query(testQuery, { id: 'uuid' }).toPromise()
 * console.log(result.data?.user.id)
 * ```
 */
export const graphqlClient = new Client({
  url: new URL('/graphql', serverEnv.SERVER_ORIGIN).href,
  exchanges: [cacheExchange, fetchExchange],
  suspense: true,
  // fetchOption で headers.cookies を指定したりするかも
})
src/_abstract/libs/gql-tada/config/graphql.ts
import type { introspection } from '@/_abstract/libs/gql-tada/__generated__/graphql-env.d.ts'
import { initGraphQLTada } from 'gql.tada'

/**
 * 型安全な GraphQL クエリを生成するための関数
 * 
 * @example
 * ```ts
 * const testQuery = `
 * query testQuery($id: ID!) {
 *   user(id: $id) {
 *     id
 *     name
 *   }
 * }
 * `
 * 
 * // const result = await graphqlClient.query(testQuery, { id: 'uuid' }).toPromise()
 * // console.log(result.data?.user.id)
 * ```
 */
export const graphql = initGraphQLTada<{
  introspection: introspection
}>()

export type { FragmentOf, ResultOf, VariablesOf } from 'gql.tada'
export { readFragment } from 'gql.tada'

参考