🙌

Next.js × urql × GraphQL Codegen(client preset)で始めるFragment Colocation

に公開

はじめに

GraphQLを採用した開発では、fragment colocation(フラグメントの局所配置)と型安全なGraphQLクエリの運用が重要な設計要素になります。

この記事では、@graphql-codegen/client-preset を使って、自動生成された型とドキュメントを活用しながら、urqlでfragmentをcolocateする方法を紹介します。

前提セットアップ

まず、必要な依存関係をインストールします:

npm install urql
npm install -D graphql @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core

codegen.ts 設定例

// codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli'

const config: CodegenConfig = {
  schema: 'http://localhost:4000/graphql',
  documents: ['app/**/*.(ts|tsx)', 'models/**/*.tsx'],
  generates: {
    './gql/': {
      preset: 'client',
      presetConfig: {
        // デフォルトだとfragmentから値を取得する関数がuseFragmentになっておりlinterにreact hooksだと勘違いされるため名前を変える
        fragmentMasking: { unmaskFunctionName: 'getFragmentData' }
      }
    },
  },
}
export default config

実行:

npx graphql-codegen

これで、gql/ ディレクトリに graphql 関数付きの型付きドキュメントが生成されます。

Fragment Colocationの基本

Fragmentを定義

// models/user.ts
import { graphql } from '../gql'
import { FragmentType, getFragmentData } from '../gql'

export const UserFullNameFragment = graphql(`
  fragment UserFullNameFragment on User {
    firstName
    lastName
  }
`)
export const fullName = (fragment: FragmentType<typeof UserFullNameFragment>)=>{
  const user = getFragmentData(UserFullNameFragment, fragment)
  return `${user.lastName} ${user.firstName}`
}

fragmentにより関数fullNameが欲しいフィールドだけを選択して取得できます。

ページでFragmentを統合したクエリを定義

// app/_graphql.ts
import { graphql } from '../gql'

export const GetUser = graphql(`
  query GetUser {
    viewer {
      loginUser {
        avatarUrl
        ...UserFullNameFragment
      }
    }
  }
`)
// app/page.tsx
'use client'
import { graphql } from '../gql'
import { useQuery } from 'urql'
import { fullName } from '../models/user'
import { GetUser } from "./_graphql"

export default function Page() {
  const [{ data, fetching }] = useQuery({ query: GetUser })

  if (fetching || !data) return <p>Loading...</p>

  return <div>
     <img src={data.viewer.loginUser.avatarUrl} alt="avatar" />
     <div>
       {fullName(data.viewer.loginUser)}
     </div>
   </div>
}

graphqlを記述・変更するたびに npx graphql-codegen を実行して型の再生成を行なってください。

すると型は全て自動で補完されるので、どのフィールドが必要か・足りていないかが型エラーで分かります。

MSWを使ったテスト(型安全)

1. MSWハンドラー定義(型付き)

// test/msw.ts
import { setupServer } from 'msw/node';

export const server = setupServer();
// test/setup.ts
import { afterAll, afterEach, beforeAll } from 'vitest';
import '@testing-library/jest-dom/vitest';
import { server } from './msw';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

2. テストコード例

// models/user.test.ts
import { fullName, UserFullNameFragment } from "./user";
import { makeFragmentData } from "@/gql";

describe("fullName", () => {
    it("ユーザーのフルネームを返す", () => {
        const user = {
        firstName: "John",
        lastName: "Doe",
        };
        const result = fullName(makeFragmentData(user, UserFullNameFragment));
        expect(result).toBe("Doe John");
    });
 });
// app/page.test.tsx
import { render, screen } from '@testing-library/react'
import Page from './page'
import { Provider, createClient } from 'urql'
import { server } from '@/test/msw'
import { graphql, HttpResponse } from 'msw'
import { GetUser } from "./_graphql"

const client = createClient({
  url: 'http://localhost/graphql',
  exchanges: [],
  fetch: fetch,
})

test('ログインユーザーのフルネームが表示される', async () => {
  server.use(
    graphql.query(GetUser, (req, res, ctx) => {
      return HttpResponse.json({
        data:{
          viewer: {
            loginUser: {
              firstName: "太郎",
              lastName: "田中",
            },
          },
        },
      })
    }),
  )
  render(
    <Provider value={client}>
      <Page />
    </Provider>
  )

  expect(await screen.findByText('田中 太郎')).toBeInTheDocument()
})

まとめ

  • @graphql-codegen/client-preset と urql を組み合わせることで、fragment colocationを型安全に実現できる
  • fragmentを使うことで、再利用性・保守性が大幅に向上

Discussion