🙌
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