Интеграция GraphQL с RTK Query позволяет автоматически создавать типизированные endpoints на основе схемы GraphQL. Такой подход избавляет от ручного описания большого количества запросов, мутаций и типов, особенно в крупных проектах с десятками сущностей.
Автогенерация особенно полезна в следующих случаях:
Типичная схема генерации выглядит следующим образом:
GraphQL Schema
↓
GraphQL Code Generator
↓
RTK Query endpoints
↓
React hooks
↓
Использование в компонентах
В основе процесса лежат три инструмента:
| Инструмент | Назначение |
|---|---|
| GraphQL Schema | Описание API |
| GraphQL Code Generator | Генерация кода |
| RTK Query | Выполнение запросов и кеширование |
Для генерации RTK Query API обычно устанавливаются следующие пакеты:
npm install @reduxjs/toolkit graphql
Пакеты генерации:
npm install -D @graphql-codegen/cli
Плагины:
npm install -D \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-rtk-query
Если используется schema introspection через URL:
npm install -D cross-fetch
Пример структуры:
src/
├── app/
│ └── store.ts
│
├── services/
│ ├── api.ts
│ ├── generated.ts
│ └── graphql/
│ ├── queries.graphql
│ └── mutations.graphql
│
├── codegen.yml
└── package.json
Пример схемы:
type User {
id: ID!
name: String!
email: String!
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
RTK Query требует базового API, в который позже будут инжектироваться endpoints.
// services/api.ts
import { createApi } from '@reduxjs/toolkit/query/react'
import { graphqlRequestBaseQuery } from '@rtk-query/graphql-request-base-query'
export const api = createApi({
reducerPath: 'api',
baseQuery: graphqlRequestBaseQuery({
url: 'https://api.example.com/graphql',
}),
endpoints: () => ({}),
})
Для GraphQL чаще всего используется:
npm install graphql-request
npm install @rtk-query/graphql-request-base-query
query GetUsers {
users {
id
name
email
}
}
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
id
name
email
}
}
schema: https://api.example.com/graphql
documents:
- "./src/services/graphql/**/*.graphql"
generates:
./src/services/generated.ts:
plugins:
- typescript
- typescript-operations
- typescript-rtk-query
config:
importBaseApiFrom: './api'
exportHooks: true
overrideExisting: true
Генерирует базовые типы GraphQL.
Пример:
export type User = {
id: string
name: string
email: string
}
Создает типы для query и mutation операций.
Пример:
export type GetUsersQuery = {
users: User[]
}
Создает RTK Query endpoints и React hooks.
Пример:
export const injectedRtkApi = api.injectEndpoints({
endpoints: (build) => ({
GetUsers: build.query<GetUsersQuery, GetUsersQueryVariables>({
query: (variables) => ({
document: GetUsersDocument,
variables,
}),
}),
}),
})
Запуск генерации:
npx graphql-codegen
После выполнения появится файл:
src/services/generated.ts
Сгенерированный файл обычно содержит:
Code Generator создает hooks автоматически:
export const {
useGetUsersQuery,
useGetUserQuery,
useCreateUserMutation,
} = injectedRtkApi
import { useGetUsersQuery } from '@/services/generated'
export function UsersPage() {
const { data, isLoading } = useGetUsersQuery()
if (isLoading) {
return <div>Loading...</div>
}
return (
<div>
{data?.users.map((user) => (
<div key={user.id}>
{user.name}
</div>
))}
</div>
)
}
import { useCreateUserMutation } from '@/services/generated'
export function CreateUserForm() {
const [createUser, { isLoading }] =
useCreateUserMutation()
const handleSubmit = async () => {
await createUser({
name: 'Alex',
email: 'alex@example.com',
})
}
return (
<button onCl ick={handleSubmit}>
Create
</button>
)
}
Генерация автоматически создает типы аргументов:
export type GetUserQueryVariables = Exact<{
id: Scalars['ID']
}>
Это исключает ошибки передачи неправильных параметров.
Ответ query полностью типизирован:
const { data } = useGetUserQuery({
id: '1',
})
data?.user?.email
IDE автоматически подсказывает поля.
Fragments уменьшают дублирование.
fragment UserFields on User {
id
name
email
}
query GetUsers {
users {
...UserFields
}
}
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) {
...UserFields
}
}
Code Generator автоматически создает:
export type UserFieldsFragment = {
id: string
name: string
email: string
}
export const api = createApi({
reducerPath: 'api',
baseQuery: graphqlRequestBaseQuery({
url: 'https://api.example.com/graphql',
prepareHeaders: (headers) => {
const token = localStorage.getItem('token')
if (token) {
headers.set(
'authorization',
`Bearer ${token}`
)
}
return headers
},
}),
endpoints: () => ({}),
})
Вместо URL можно использовать локальный файл:
schema: ./schema.graphql
Сначала можно скачать схему:
npx graphql-codegen introspection
После этого:
schema: ./schema.json
Для постоянной генерации:
npx graphql-codegen --watch
Это удобно при активной разработке backend схемы.
RTK Query использует injectEndpoints.
Сгенерированный код обычно выглядит так:
export const injectedRtkApi =
api.injectEndpoints({
endpoints: (build) => ({
GetUsers: build.query({
query: () => ({
document: GetUsersDocument,
}),
}),
}),
})
Такой подход позволяет:
GraphQL Code Generator поддерживает преобразование имен.
config:
namingConvention:
typeNames: change-case-all#pascalCase
enumValues: change-case-all#upperCase
Если React hooks не нужны:
config:
exportHooks: false
Иногда требуется преобразование данных.
Можно расширить generated endpoints:
const extendedApi = injectedRtkApi.enhanceEndpoints({
endpoints: {
GetUsers: {
transformResponse: (response) => {
return response.users
},
},
},
})
GraphQL Scalars могут быть сопоставлены с TypeScript типами.
scalar DateTime
config:
scalars:
DateTime: string
config:
scalars:
DateTime: Date
JSON: Record<string, unknown>
GraphQL enum:
enum UserRole {
ADMIN
USER
}
Code Generator:
export enum UserRole {
Admin = 'ADMIN',
User = 'USER',
}
GraphQL:
union SearchResult = User | Post
Generated types:
export type SearchResult =
| User
| Post
query Search {
search {
__typename
... on User {
id
name
}
... on Post {
id
title
}
}
}
Code Generator создает discriminated unions:
if (item.__typename === 'User') {
item.email
}
В больших проектах generated.ts может стать огромным.
Распространенный подход:
generates:
./src/services/types.ts:
plugins:
- typescript
./src/services/:
preset: near-operation-file
presetConfig:
extension: .generated.ts
baseTypesPath: types.ts
plugins:
- typescript-operations
- typescript-rtk-query
Структура:
graphql/
├── users.query.graphql
├── users.query.generated.ts
├── posts.query.graphql
└── posts.query.generated.ts
Преимущества:
Дополнительно можно генерировать SDK:
plugins:
- typescript
- typescript-operations
- typescript-graphql-request
RTK Query позволяет использовать одновременно:
Пример:
export const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
endpoints: () => ({}),
})
Отдельный GraphQL API:
export const graphqlApi = createApi({
baseQuery: graphqlRequestBaseQuery({
url: '/graphql',
}),
endpoints: () => ({}),
})
RTK Query поддерживает автоматическую invalidation систему.
config:
addTagTypes: true
Generated endpoint:
providesTags: ['User']
Mutation:
invalidatesTags: ['User']
Code Generator поддерживает:
config:
exposeQueryKeys: true
exposeDocument: true
exposeMutationKeys: true
Позволяет получить GraphQL document:
GetUsersDocument
Это полезно для:
Сгенерированные endpoints можно использовать в SSR.
Пример prefetch:
store.dispatch(
api.endpoints.GetUsers.initiate()
)
RTK Query поддерживает lazy loading endpoints.
const usersApi = api.injectEndpoints({
endpoints: (build) => ({
GetUsers: build.query({
query: () => ({
document: GetUsersDocument,
}),
}),
}),
})
{
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen --watch"
}
}
Codegen часто запускается:
Пример:
{
"scripts": {
"build": "npm run codegen && vite build"
}
}
GraphQL schema drift может ломать frontend.
Популярная практика:
graphql-inspector diff
Ошибка:
Unable to find any GraphQL type definitions
Причина:
documents:
- "./src/**/*.graphql"
не находит файлы.
Причины:
Решения:
GraphQL:
type Query
может конфликтовать с локальными типами.
Решение:
config:
typesPrefix: Gql
export const injectedRtkApi =
api.injectEndpoints({
endpoints: (build) => ({
GetUser: build.query<
GetUserQuery,
GetUserQueryVariables
>({
query: (variables) => ({
document: GetUserDocument,
variables,
}),
}),
CreateUser: build.mutation<
CreateUserMutation,
CreateUserMutationVariables
>({
query: (variables) => ({
document: CreateUserDocument,
variables,
}),
}),
}),
})
Без генерации требуется вручную:
Code Generator автоматизирует эти задачи.
Любое изменение схемы немедленно отражается в типах.
Ошибки обнаруживаются:
GraphQL schema становится центральным контрактом между frontend и backend.
Автоматически появляются:
Крупные схемы могут генерировать десятки тысяч строк.
Интеграция требует:
Любые breaking changes backend могут ломать frontend генерацию.
Распространенная структура:
src/
├── shared/api/
│ ├── baseApi.ts
│ ├── graphql/
│ │ ├── user/
│ │ ├── post/
│ │ └── comment/
│ └── generated/
config:
exportHooks: true
exposeDocument: true
exposeQueryKeys: true
addTagTypes: true
avoidOptionals: true
immutableTypes: true
maybeValue: T | null
Генерация readonly типов:
readonly id: string
Это уменьшает риск случайных мутаций.
Вместо:
name?: string
генерируется:
name: string | null
Такой подход лучше соответствует GraphQL semantics.
Настройка nullable типов:
maybeValue: T | null
или:
maybeValue: T | undefined
GraphQL Backend
↓
Schema
↓
GraphQL Code Generator
↓
RTK Query endpoints
↓
Generated hooks
↓
React components
↓
Automatic cache management
↓
Type-safe frontend