Фрагменты и директивы

Фрагменты позволяют один раз описать набор полей и переиспользовать его, а директивы @include и @skip — включать или выключать поля прямо в запросе по условию.

Не повторяйся в запросах: то, что в коде делают функции, во фрагментах делают именованные наборы полей.

Когда несколько частей запроса просят один и тот же набор полей, копировать его — путь к ошибкам. Фрагмент — это именованный кусок выборки на конкретном типе, который можно подставлять через ...:

fragment UserCard on User {
  id
  name
  avatarUrl
}

query {
  me { ...UserCard }
  user(id: "42") { ...UserCard }
}

Теперь карточка пользователя описана один раз. Поменяешь фрагмент — изменится во всех местах сразу. Это особенно важно на клиенте: компоненты часто объявляют собственные фрагменты с нужными им полями, а страница собирает из них общий запрос. Такой подход называют co-location (соседство): фрагмент с нужными данными лежит рядом с компонентом, который их рисует. Когда компонент меняет свои потребности в данных, он правит только свой фрагмент — а родительская страница автоматически запросит обновлённый набор полей. Так UI и его запросы данных эволюционируют вместе, не разъезжаясь; это один из приёмов, благодаря которым GraphQL так удобно ложится на компонентную архитектуру React, Vue и подобных фреймворков.

Inline-фрагменты

Если поле возвращает интерфейс или объединение (union), inline-фрагмент позволяет выбрать поля в зависимости от конкретного типа:

query {
  search(text: "graphql") {
    ... on User { name }
    ... on Post { title }
  }
}

Директивы @include и @skip

Директивы меняют выполнение запроса. Две встроенные — @include(if:) и @skip(if:) — позволяют условно включать поля, не переписывая текст запроса:

query GetUser($withEmail: Boolean!) {
  user(id: "42") {
    name
    email @include(if: $withEmail)
    phone @skip(if: $withEmail)
  }
}

Если $withEmail = true, в ответ попадёт email, а phone пропустится; при false — наоборот. Один запрос гибко подстраивается под контекст.

Как работает под капотом

Фрагменты сервер «разворачивает» — подставляет их поля на место ...Имя ещё до выполнения. Директивы @include/@skip вычисляются на этапе разбора: поле либо остаётся в дереве, либо вырезается до вызова резолверов. Смоделируем оба механизма на JS:

// фрагмент = переиспользуемый список полей
const UserCard = ["id", "name", "avatarUrl"];

const user = {
  id: 42, name: "Аня", avatarUrl: "a.png",
  email: "[email protected]", phone: "+7..."
};

function buildSelection(withEmail) {
  // разворачиваем фрагмент
  const fields = [...UserCard];
  // применяем директивы
  if (withEmail) fields.push("email");   // @include(if: withEmail)
  else fields.push("phone");             // @skip(if: withEmail)
  return fields;
}

function pick(obj, fields) {
  return fields.reduce((a, f) => (a[f] = obj[f], a), {});
}

console.log(pick(user, buildSelection(true)));  // ...карточка + email
console.log(pick(user, buildSelection(false))); // ...карточка + phone

Попробуй сам ▶ — добавь во фрагмент UserCard поле "name" дважды или новое поле и посмотри, как оно появится сразу в обоих ответах. Так фрагмент централизует выборку.

Частые ошибки

  • Фрагмент на неподходящем типе. fragment X on User нельзя применить к полю, возвращающему Post — типы должны совпадать.
  • Путать @skip и @include. @skip(if: true) убирает поле, @include(if: true) оставляет. Лёгкий способ запутаться и получить не то.
  • Дублировать поля вместо фрагмента. Копипаст выборки в нескольких запросах рано или поздно разъедется и даст несогласованность.

Best practices

  • Объявляй фрагменты рядом с UI-компонентами, которым нужны эти поля — это паттерн «colocation» в Apollo и Relay.
  • Имя фрагмента делай говорящим (UserCard, PostSummary): по нему сразу понятно, для какого блока интерфейса он нужен.
  • Используй @include/@skip для опциональных тяжёлых полей, которые нужны не на каждом экране.

Итоги

Фрагменты — это переиспользуемые наборы полей на конкретном типе, подключаемые через ...; они убирают дублирование и лежат в основе компонентного подхода клиентов. Директивы @include и @skip условно включают поля прямо в запросе. Дальше переходим к изменению данных — мутациям.

Проверьте себя
1. Для чего нужны фрагменты?
AЧтобы изменять данные
BЧтобы один раз описать набор полей и переиспользовать его в разных местах запроса
CЧтобы ускорить сервер
DЧтобы задать переменные
2. Что произойдёт при email @skip(if: true) ?
AПоле email обязательно появится
BПоле email будет пропущено и не попадёт в ответ
CЗапрос упадёт с ошибкой
Demail станет переменной