Раскройте возможности фрагментов с помощью GraphQL Codegen


Эта статья была опубликована в пятницу, 5 августа 2022 года, автором Laurin Quast @ The Guild Blog

Несомненно, Relay является самым продвинутым JavaScript GraphQL клиентом. Однако, в то же время, кривая обучения и внедрения еще очень слаба по сравнению с другими популярными альтернативами, такими как Apollo Client, urql или GraphQL Request.

Как следствие, многие люди даже не знают обо всех преимуществах и паттернах, используемых в Relay (и пока что исключительно в нем).
Гильдия собирала и работала над проектами, в которых использование GraphQL-клиента охватывает все доступные сегодня решения.
На наш взгляд, самые важные части Relay — это концепции построения и масштабирования приложений.
Эти паттерны фактически применимы к любому GraphQL-клиенту, вам просто нужен правильный инструмент генерации кода для работы.

На высоком уровне эти преимущества заключаются в следующем:

Фрагментированное дерево компонентов. Вместо того чтобы писать один документ операции GraphQL для каждого видимого компонента или извлекать свойства компонента из одного типа документа операции запроса, что часто бывает громоздким), можно составить несколько определений фрагментов до одной операции запроса, которая отправляется на сервер.
Это уменьшает количество одновременных запросов и вместе с @defer и @stream является фактическим улучшением производительности приложения (по сравнению с пакетными операциями GraphQL).

Размещайте документы GraphQL в коде компонента. Вместо того чтобы писать операции GraphQL в специальном файле, операции пишутся в коде компонента. Случалось ли вам удалять код компонента и забывать удалить соответствующие операции или определения фрагментов, которые находятся в каком-то другом поле?
Мы сталкивались с этой проблемой снова и снова, создавая множество мертвого кода.

import { gql, useFragment, FragmentType } from './gql'

const Avatar_UserFragment = gql(/* GraphQL */ `
  fragment Avatar_UserFragment on User {
    avatarUrl
  }
`)

type AvatarProps = {
  user: FragmentType<typeof Avatar_UserFragment>
}

export function Avatar(props: AvatarProps) {
  const user = useFragment(Avatar_UserFragment, props.user)
  return <CircleImage src={user.avatarUrl} />
}
Вход в полноэкранный режим Выход из полноэкранного режима

Маскировка данных (фрагментов). Убедитесь, что компонент может получить доступ только к данным, определенным в определениях его фрагментов, чтобы сохранить компонент как самостоятельный строительный блок, который можно использовать повторно.

Мы рады сообщить, что теперь у нас есть пресет для внедрения вышеуказанных паттернов Relay в ваши существующие проекты, независимо от того, какую клиентскую библиотеку вы сейчас используете, без необходимости переходить на экосистему Relay.

Новый пресет GraphQL Code Generator

Новый пресет служит заменой для всех плагинов, генерирующих хуки для существующих клиентов, таких как urql и apollo-client.

yarn add -D @graphql-codegen/gql-tag-operations-preset
Вход в полноэкранный режим Выход из полноэкранного режима
schema: ./schema.graphql
documents:
  - 'src/**/*.ts'
  - '!src/gql/**/*'
generates:
  ./src/gql/:
    preset: gql-tag-operations-preset
Войти в полноэкранный режим Выход из полноэкранного режима

Вместо того чтобы генерировать хуки для существующих клиентов, предустановка работает с перегрузкой сигнатур функций и TypedDocumentNode) для вывода правильной операции GraphQL и типа переменных.

Вы можете узнать больше о TypedDocumentNode в graphql.wtf эпизод #41.

Теперь в коде вашего приложения вы эффективно напишите следующий код.

import { useQuery } from 'urql'
import { gql } from './gql'

const ViewerQueryDocument = gql(/* GraphQL */ `
  query ViewerQuery {
    viewer {
      id
      name
    }
  }
`)

const Viewer = () => {
  const [result] = useQuery({ query: ViewerQueryDocument })
  const { data, fetching, error } = result

  if (fetching) return <p>Loading…</p>
  if (error) return <p>Oh no… {error.message}</p>
  // data is fully typed!
  return <p>{data?.viewer?.name}</p>
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Поскольку клиент имеет встроенную поддержку TypedDocumentNode, он извлечет правильный тип операции и переменных из документа, переданного в useQuery.

За более подробной информацией обратитесь к документации GraphQL Code Generator.

Теперь это наш рекомендуемый способ использования GraphQL Code Generator для разработки фронт-энда.

Описывайте потребности компонентов в данных с помощью фрагментов GraphQL

Вместо того чтобы писать одну большую операцию GraphQL для всей страницы и передавать ее компонентам, начните с описания зависимостей данных компонента с помощью фрагмента GraphQL.
Таким образом, вы сделаете зависимость данных вашего компонента размещенной и явной точно так же, как некоторые размещают определения TypeScript или CSS, если вы используете паттерн стилизованных компонентов.

import { gql, useFragment, FragmentType } from './gql'

const Avatar_UserFragment = gql(/* GraphQL */ `
  fragment Avatar_UserFragment on User {
    avatarUrl
  }
`)

type AvatarProps = {
  user: FragmentType<typeof Avatar_UserFragment>
}

export function Avatar(props: AvatarProps) {
  const user = useFragment(Avatar_UserFragment, props.user)
  return <CircleImage src={user.avatarUrl} />
}
Вход в полноэкранный режим Выход из полноэкранного режима

Ограничение доступа к данным с помощью фрагментов

Используя хук useFragment, мы гарантируем, что компонент может получить доступ только к тем свойствам данных, которые объявлены непосредственно в наборе выбора фрагментов. К другим данным, объявленным через дополнительный набор фрагментов (Avatar_UserFragment), нельзя получить доступ в UserListItem.

Следующий пример вызовет ошибку TypeScript, поскольку поле avatarUrl не выбрано внутри фрагмента UserListItem_UserFragment.

import { gql, useFragment, FragmentType } from './gql'
import { Avatar } from './avatar'

const UserListItem_UserFragment = gql(/* GraphQL */ `
  fragment UserListItem_UserFragment on User {
    fullName
    ...Avatar_UserFragment
  }
`)

type UserListItemProps = {
  user: FragmentType<typeof UserListItem_UserFragment>
}

export function UserListItem(props: UserListItemProps) {
  const user = useFragment(Item_UserFragment, props.user)

  // ERROR: Property 'avatarUrl' does not exist on type
  const icon = <img src={user.avatarUrl} />
  return <ListItem icon={icon}>{user.fullName}</ListItem>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Компоновка фрагментов для компонентов пользовательского интерфейса

Как мы компонуем простые визуальные компоненты пользовательского интерфейса в более крупные функциональные компоненты пользовательского интерфейса, мы также можем компоновать фрагменты потребляемого компонента пользовательского интерфейса.

import { gql, useFragment, FragmentType } from './gql'
import { Avatar } from './avatar'

const UserListItem_UserFragment = gql(/* GraphQL */ `
  fragment UserListItem_UserFragment on User {
    fullName
    ...Avatar_UserFragment
  }
`)

type UserListItemProps = {
  user: FragmentType<typeof UserListItem_UserFragment>
}

export function UserListItem(props: UserListItemProps) {
  const user = useFragment(Item_UserFragment, props.user)
  return <ListItem icon={<Avatar user={user} />}>{user.fullName}</ListItem>
}
Вход в полноэкранный режим Выход из полноэкранного режима

Компоновка фрагментов компонентов для маршрута или представления верхнего уровня

Теперь мы можем продолжать компоновать наши компоненты, пока не достигнем компонентов, которые выбирают тип объекта корневого запроса Query.

import { gql, useFragment, FragmentType } from './gql'
import { UserListItem } from './user-list-item'

const FriendList_QueryFragment = gql(/* GraphQL */ `
  fragment FriendList_QueryFragment on Query {
    friends(first: 5) {
      id
      ...UserListItem_UserFragment
    }
  }
`)

type FriendListProps = {
  query: FragmentType<typeof FriendList_QueryFragment>
}

export function FriendList(props: FriendListProps) {
  const query = useFragment(FriendList_QueryFragment, props.query)
  return (
    <List>
      {query.friends.map(user => (
        <FriendListItem key={user.id} user={user} />
      ))}
    </List>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Ранее мы объявили компонент Avatar с помощью фрагмента Avatar_UserFragment. Теперь мы можем повторно использовать этот Аватар в другом контексте, снова распространив фрагмент и передав соответствующие данные в свойство Avatar компонента user.

import { gql, useFragment, FragmentType } from './gql'

const UserProfileHeader_QueryFragment = gql(/* GraphQL */ `
  fragment UserProfileHeader_QueryFragment on Query {
    viewer {
      id
      homeTown
      registeredAt
      ...Avatar_UserFragment
    }
  }
`)

type UserProfileHeaderProps = {
  query: FragmentType<typeof UserProfileHeader_QueryFragment>
}

export function UserProfileHeader(props: UserProfileHeaderProps) {
  const query = useFragment(UserProfileHeader_QueryFragment, props.query)
  return (
    <>
      <Avatar user={query.viewer} />
      {query.viewer.homeTown}
      {query.viewer.registeredAt}
    </>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь у нас есть два компонента пользовательского интерфейса, которым требуются данные из корневого типа Query, FriendList и UserProfileHeader .

Компонуйте все фрагменты запросов в одну операцию Query

Наконец, мы можем объединить все наши фрагменты в одну операцию GraphQL Query, которая получает все данные по нашему компоненту маршрута. Это позволяет нам эффективно получить все данные, необходимые для маршрута, за один обход сервера.

import { gql, useFragment, FragmentType } from './gql'
import { UserProfileHeader } from './user-profile-header'
import { FriendList } from './friend-list'

const UserProfileRoute_Query = gql(/* GraphQL */ `
  query UserProfile_Query {
    ...UserProfileHeader_QueryFragment
    ...UserList_QueryFragment
  }
`)

export function UserProfileRoute_Query() {
  const { data, loading, error } = useQuery(UserProfile_Query)
  if (loading) return <Loading />
  if (error) return <Error />
  return (
    <>
      <FriendList query={data} />
      <UserProfileHeader query={data} />
    </>
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

Наконец, мы имеем следующее дерево компонентов, как уже упоминалось ранее.

Заключение

Этот паттерн позволяет вам повторно использовать и максимально масштабировать ваши компоненты, имея при этом четкий и разумный поток зависимостей данных от верхнего маршрута до концов дерева компонентов пользовательского интерфейса.

Вы можете начать применять этот паттерн в вашем существующем GraphQL приложении с любым GraphQL клиентом, который поддерживает TypedDocumentNode с помощью нашего нового и проверенного в боях пресета gql-tag-operations-preset GraphQL Code Generator.

Все следующие клиенты поддерживаются с любым фреймворком (React, Vue, Angular и т.д.)

  • urql
  • Клиент Apollo
  • «Ванильный» Node.js
  • graphql-request (мы недавно добавили поддержку).

Вы можете быстро начать работу без особых настроек, следуя предварительно подготовленной документации.

Возможно, это даже вдохновит вас углубиться в оптимизацию клиентской среды Relay поверх этих шаблонов и убедит вас использовать Relay в вашем следующем проекте. Вы можете узнать больше о них на сайте relay.dev.

Мы, как Гильдия, хотим, чтобы вы нашли инструмент, подходящий для вашего опыта и приложения, которое вы создаете. Самое главное — это паттерны, которые применимы везде!

Не хватает?

Недавно я также говорил на эту тему на встрече GraphQL Berlin!

Оцените статью
devanswers.ru
Добавить комментарий