Facebook в прошлом проделывал действительно отличные вещи с Relay. И они быстро над этим работают. Когда я начал писать эту статью, Relay был в v8. И прежде чем я смогу закончить, реле находится в v9. Стоит проверить количество версий, вышедших за последние 12 месяцев. По версиям они добавили отличную функциональность. Например, экспериментальная версия имеет хуки и улучшенную интеграцию саспенса, которая работает с параллельным режимом React. К сожалению, люди в Facebook не приложили много усилий для правильного документирования Relay. Примеры в документации местами разбиты, а сама документация неполна. Он пропускает много важных вещей, таких как добавление пользовательских скаляров в компилятор Relay и правильное объяснение Relay store.

Это моя попытка изучить магазин Relay. Большая часть содержания здесь основана на моем опыте работы с Relay на работе и в личных проектах.

Для кого эта статья?

Эта статья предназначена для людей, которые могут использовать Relay, но с трудом работают с Relay store. Предпосылки для этой статьи:

  1. ES6 - модули и более новые операторы, такие как необязательное объединение в цепочку и нулевое объединение. Это просто причудливые названия относительно простых операторов. Они хорошо поддерживаются в TypeScript 3.8.
  2. Знание GraphQL
  3. Базовый опыт работы с Реле
  4. [Необязательно] Машинопись - основы и немного общих типов

Чтобы понять эту статью, вы должны уметь работать с Relay. Вы должны уметь писать запросы и изменения, а также использовать различные контейнеры. Вы также должны быть знакомы со спецификациями, обеспечиваемыми Relay - Глобальная идентификация объекта и Спецификация разбивки на страницы. Вы можете найти большую часть этого материала в документации по Relay. Наконец, вам необходимо иметь четкое представление о функциях средства обновления.

Как читать эту статью?

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

Для начала вам следует рассмотреть следующие два раздела. Что касается более поздних разделов, может быть утомительно читать их все за один присест. Так что не стесняйтесь пропускать разделы и переходить к интересующей вас части. Я предлагаю рекурсивную стратегию обхода. Он включает в себя следующие этапы:

  1. Начните с раздела, который хотите прочитать.
  2. Если в какой-то момент этот раздел использует содержимое из другого раздела, не объясняя его должным образом, обратитесь к другому разделу.
  3. Повторяйте вышеуказанные шаги, пока не поймете все, с чем столкнетесь.

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

Примечание. Хотя я настоятельно рекомендую вам читать статью выборочно, есть некоторые темы, зависящие от цикла. Я объяснил это везде, где это было необходимо, но все же советую читать с осторожностью.

1. Что такое магазин реле?

Операция GraphQL возвращает некоторую полезную нагрузку данных. Операция здесь может быть запросом, изменением или подпиской. Эта полезная нагрузка доступна с помощью Relay, задачей которого является отправка этих данных в соответствующие компоненты. Relay хранит все данные в центральном хранилище, называемом «магазин». Хранилище Relay - это структура данных, отвечающая за хранение всех данных GraphQL в вашем приложении.

Примечание. Для правильной работы хранилища ретрансляторов ваша схема должна быть Relay-совместимой. То есть он должен соответствовать шаблону Глобальная идентификация объекта. Каждый базовый тип должен иметь уникальный идентификатор. Кроме того, для более удобной разбивки на страницы, вся разбивка на страницы должна соответствовать спецификации реле.

Магазин запоминает ваши операции и объекты GraphQL, возвращаемые этими операциями. Если бы вы зашли в магазин, вы бы увидели это

Интересно, как я в приставку попал магазин? Я вам подскажу.

updater(store) => {window["store"] = store; ...}

Слишком много, не правда ли? К счастью, у нас есть Relay DevTools, который поможет вам лучше изучить магазин.

При этом Relay DevTools довольно глючит и не всегда надежен. Но что-то лучше, чем ничего, правда?

1.1 Магазин против кеша

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

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

1.2 Зачем мне нужно использовать магазин?

Если вы хотите обновить данные на стороне клиента в результате операции GraphQL, вам необходимо изменить хранилище. Relay следит за тем, чтобы новые данные доходили до нужного места.

Представьте, что у нас есть приложение. У него есть страница, на которой я могу обновить данные пользователя. Чтобы выполнить обновление, я запускаю мутацию, сообщающую серверу, что нужно изменить. Затем я беру полезную нагрузку из этой мутации и обновляю ее для текущего пользователя в магазине. И вуаля! Мое приложение теперь повсюду обновляет информацию для этого пользователя.

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

2. Базовый сценарий

Представьте себе приложение для онлайн-викторин (как на скриншотах). Вот базовая схема приложения.

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

Примечание. Из-за отсутствия надлежащих инструментов форматирования фрагменты кода были вручную смещены, и формат может нарушаться на экранах меньшего размера. Я мог бы разместить здесь скриншоты, но это лишает смысла фрагменты кода. Вы можете скопировать и вставить их в свою среду IDE для удобства чтения.

query AppQuery{
  # Getting the logged in user
  # viewer: User!
  viewer{
    id 
    name
  }

  # Getting a user by their ID
  # user(id: ID!): User
  user(id: "ANOTHERUSER1"){
    id
    name
  }

  # A query to get user by ID
  # Returns ComplicatedResponse type - type with nesting
  # complicatedUser(userId: ID!): ComplicatedResponse!
  complicatedUser(userId: "ANOTHERUSER2"){
    id # ID of ComplicatedResponseType
    node{
      id # ID of the User
      name
    }
  }
  # Getting a question by its ID
  # question(id: ID!): Question
  question(id: "QUESTION1"){
    id
    description(locale: "fr")
  }
}

Когда вы запустите запрос, в магазине будут следующие записи:

  1. Запись типа User с идентификатором вошедшего в систему пользователя (say"VIEWERID")
  2. Другая запись типа User с идентификатором "ANOTHERUSER1"
  3. Другая запись типа User с идентификатором "ANOTHERUSER2"
  4. Запись типа Question с идентификатором "QUESTION1"
  5. Запись для viewer, которая указывает на запись, указанную в 1.
  6. Запись для user(id: “ANOTHERUSER1”), которая указывает на запись, упомянутую в 2.
  7. Предположим, что идентификатор типа complicatedResponse “COMPLICATEDRESPONSE”. В магазине есть запись для complicatedUser(userId: “ANOTHERUSER2”), которая указывает на запись с идентификатором “COMPLICATEDRESPONSE”. Поле node этой записи указывает на запись, упомянутую в 3.
  8. Запись для question(id: “QUESTION1”), которая указывает на запись, указанную в 4.

Найдите минутку. Прочтите все это еще раз как следует. Поймите все, что я сказал до сих пор. Это будет основой всей нашей дискуссии. В настоящее время магазин можно отобразить, как показано ниже:

(ref Means reference)
"VIEWERID" -> id: "VIEWERID", ..., type: "User",
"ANOTHERUSER1" -> id: "ANOTHERUSER1", ..., type: "User",
"ANOTHERUSER2" -> id: "ANOTHERUSER2", ..., type: "User",
"QUESTION1" -> id: "QUESTION1", ..., type: "Question"
"COMPLICATEDRESPONSE" -> id: "COMPLICATEDRESPONSE", type: "ComplicatedResposne", ..., (node -> ref "ANOTHERUSER2")
viewer -> ref "VIEWERID"
user(id: “ANOTHERUSER1”) -> ref "ANOTHERUSER1"
complicatedUser(userId: “ANOTHERUSER2”) -> ref "COMPLICATEDRESPONSE"
question(id: “QUESTION1”) -> ref "QUESTION1"

Мы все настроены. У нас есть схема и есть магазин. Магазин был заполнен несколькими записями. Если вы заметили схему, есть и другие запросы и изменения. Мы будем использовать их позже, чтобы проиллюстрировать соответствующие концепции.

2.3 Три важных интерфейса

Документация по реле охватывает три интерфейса для работы с магазином. А пока думайте об интерфейсах как о наборе методов и свойств, которые не предоставляет объект. Интерфейс решает, как взаимодействовать с соответствующим объектом. Интерфейсы реле перечислены ниже:

  1. RecordSourceSelectorProxy: Экземпляр магазина, предоставляемого функциям обновления. Используется, чтобы забирать записи из магазина.
  2. RecordProxy: экземпляр данной записи. Используется для просмотра и изменения записи
  3. ConnectionHandler: Он не определен как интерфейс, но представлен как модуль с экспортированными методами. Итак, мы рассматриваем интерфейс, предоставляемый модулем.
import {ConnectionHandler} from "relay-runtime";

Когда я представляю интерфейсы, я представляю типизацию, предоставляемую пакетом relay-runtime, а также определения интерфейсов, приведенные в документации. Типы пакетов более наглядны, чем интерфейсы в документации Relay. При обсуждении методов я использую сигнатуры методов из документации Relay, а также типизацию. Вы должны ссылаться на наиболее естественную для вас подпись. Если подпись вам непонятна, вам следует обратиться к интерфейсам, определенным в соответствующем введении.

Примечание. Как уже говорилось, в некоторых частях статьи я использую типизацию, предоставляемую пакетом relay-runtime. Они немного запутаны и несовместимы с документами и могут быть изменены в будущем. Я хочу, чтобы вы помнили об этом, читая статью. Вы всегда можете обратиться к наборам, приведенным в документации по ретрансляции

Типы используют тип Primitive для представления скаляров. Он определяется как string | number | boolean | null | undefined.

Примечание. Relay преобразует перечисления в объединение строк. Например, enum MyEnum{ALPHA, BETA} становится type MyEnum = "ALPHA" | "BETA". Итак, все скаляры относятся к примитивному типу. Relay также определяет тип DataId, который является псевдонимом для string. Он используется для представления id в разделе "Глобальная идентификация объекта".

Кроме того, как я уже сказал, я обсуждаю сигнатуры метода, когда прохожу через метод. Таким образом, вам действительно не нужно читать весь интерфейс. Они упомянуты только для полноты картины. При этом вам может потребоваться обратиться к интерфейсам, если сигнатура метода относится к типам, определенным в определении интерфейса. Например, рассмотрим интерфейс ниже:

interface MyInterface<T extends Record<string, any> = {}>{
  myMethod: (arg: T) => void
}

Чтобы понять myMethod, вам нужно знать о T, который определяется с помощью интерфейса.

3. RecordSourceSelectorProxy

Помните функции обновления? Я сказал, что у них есть аргумент в пользу магазина. Тип этого аргумента - RecordSourceSelectorProxy. Этот интерфейс обеспечивает контролируемый доступ к магазину, а также позволяет Relay отслеживать внесенные вами изменения. Это, в свою очередь, помогает с оптимистичными мутациями, когда вы обновляете магазин, не дожидаясь ответа сервера. В том случае, если есть ошибка при мутации, Relay может откатить хранилище до предыдущего состояния. Используя RecordSourceSelectorProxy, вы можете создавать, получать или удалять записи в магазине. Если вы создадите или получите запись, вы получите объект типа RecordProxy. Этот интерфейс помогает вам перемещаться по самой записи и управлять ею.

Теперь давайте посмотрим на методы в RecordSourceSelectorProxy. Согласно документации, интерфейс приведен ниже:

interface RecordSourceSelectorProxy {
   create(dataID: string, typeName: string): RecordProxy;
   delete(dataID: string): void;
   get(dataID: string): ?RecordProxy;
   getRoot(): RecordProxy;
   getRootField(fieldName: string): ?RecordProxy;
   getPluralRootField(fieldName: string): ?Array<?RecordProxy>;
   invalidateStore(): void;
 }

Если вы посмотрите на набор текста, поставляемый с пакетом relay-runtime, вы увидите следующее:

interface RecordSourceProxy {
  create(dataID: DataID, typeName: string): RecordProxy;
  delete(dataID: DataID): void;
  get<T = {}>(dataID: DataID): RecordProxy<T> | null | undefined;
  getRoot(): RecordProxy;
}
interface RecordSourceSelectorProxy<T = {}> extends RecordSourceProxy {
  getRootField<K extends keyof T>(fieldName: K): RecordProxy<NonNullable<T[K]>>;
  getRootField(fieldName: string): RecordProxy | null;
  getPluralRootField(fieldName: string): Array<RecordProxy<T> | null> | null;
}

Как уже говорилось, большинство методов возвращают RecordProxy. Итак, чтобы правильно понять RecordSourceSelector, вам также нужно немного знать RecordProxy. Я объяснил соответствующие биты везде, где это было необходимо. Если вы чувствуете, что этого недостаточно, смело переходите к разделу RecordProxy для справки.

3.1 get

Прототип этого метода:

Docs: get(dataID: string): ?RecordProxy
Typings: get<T = {}>(dataID: DataID): RecordProxy<T> | null | undefined;

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

3.2 getRoot

Прототип этого метода:

Docs, Typings: getRoot(): RecordProxy

Этот метод возвращает корень магазина. Отсюда вы можете получить доступ к любой записи, которую захотите. Как следует из названия, Root фактически действует как точка входа в магазин. Вы перемещаетесь по хранилищу из корневого каталога и добираетесь до нужной записи. Мы увидим это в действии позже.

Теперь я собираюсь немного обсудить RecordProxy. Позже будет раздел, посвященный RecordProxy. Но пока я просто остановлюсь на вещах, необходимых для понимания getRoot. RecordProxy по существу позволяет вам получать доступ и устанавливать поля записи.

Например, если UserRecord - это RecordProxy для viewer, я могу сделать:

  1. UserRecord.getValue("name");, чтобы узнать имя пользователя
  2. UserRecord.setValue(newNameValue ,"name");, чтобы обновить имя пользователя

Но это работает, только если в соответствующем поле указано Scalar. Рассмотрим запись complicatedUser типа complicatedResponse. Тип node не является скаляром, а вместо этого указывает на объект типа User (с идентификатором VIEWERID). Представьте себе complicatedRecord типа ComplicatedResponse. Здесь complicatedRecord.getValue("node") не будет работать, поскольку поле не является скаляром.

В случаях, когда поле является типом объекта GraphQL, вы можете использовать метод getLinkedRecord. Чтобы получить доступ к имени пользователя в поле node, вы можете сделать следующее:

const name = complicatedRecord
.getLinkedRecord("node")
.getValue("name")

Опять же, мы рассмотрим это как следует, когда будем обсуждать RecordProxy. Но пока этого достаточно. Корневой объект можно использовать для доступа к любым данным в магазине. getRootmethod возвращает RecordProxy для корня. Все записи привязаны к корню. Чтобы получить доступ к просмотру из магазина, мы можем сделать

const viewer = store
.getRoot()
.getLinkedRecord("viewer");

Это возвращает зрителю RecordProxy. Теперь вы можете сделать viewer.getValue("name"), чтобы получить имя зрителя.

Рассмотрим следующую мутацию:

mutation UpdateUserMutation{
# Update the user's name, given their ID
# updateUser(id: ID!, newName: String!): User!
  updateUser(id: "ANOTHERUSER1", newName:"Random Name"){
    id
    name
  }
}

После выполнения мутации для обновления имени пользователя в магазин добавляются следующие данные:

updateUser(id: “ANOTHERUSER1”, newName: “Random Name”) -> id: "ANOTHERUSER1", newName: "Random Name", type: "User"

Вы можете получить эту запись из магазина, выполнив

const payload = store
.getRoot()
.getLinkedRecord('updateUser(id: “ANOTHERUSER1”, newName: “Random Name”)');

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

const payload = store
.getRoot()
.getLinkedRecord("updateUser", {id: "ANOTHERUSER1", newName: "Random Name"});

Выше показан изящный способ привязки переменных к полям. В этом примере мы связали аргументы id: "ANOTHERUSER1" и newName: "Random Name" с updateUser.

3.3 getRootField

Типы реле предоставляют два прототипа для этого метода - один с универсальными шаблонами, а другой - без них. Здесь T относится к типу, переданному в RecordSourceSelectorProxy интерфейс.

Docs: getRootField(fieldName: string): ?RecordProxy
Typings: 
1. getRootField<K extends keyof T>(fieldName: K): RecordProxy<NonNullable<T[K]>>;
2. getRootField(fieldName: string): RecordProxy | null;

Этот метод также позволяет получать записи из магазина. Чем это отличается от метода getRoot? Они различаются по объему и применению. В то время как getRoot может получать данные со всего магазина, getRootField не может. Он может получить только данные, соответствующие соответствующей операции (операции, которая вызвала функцию обновления).

Например, рассмотрим мутацию updateUser:

mutation UpdateUserMutation{
  updateUser(id: "ANOTHERUSER1", newName: "Random Name"){
    id
    name
  }
}

Это приводит к следующему документу GraphQL:

updateUser(id: "ANOTHERUSER1", newName: "Random Name"){
  id
  name
}

getRootField может получать данные только из этого документа. Но хорошо то, что, поскольку это было сделано для небольших документов, вы можете напрямую получать записи - без привязки аргументов к полю.

const payload = store.getRootField("updateUser");

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

const changedName = payload.getValue("name")

3.4 getPluralRootField

Этот метод почти такой же, как getRootField, с очень тонкой разницей. Прототип этого метода:

Docs: getPluralRootField(fieldName: string): ?Array<?RecordProxy>
Typings: getPluralRootField(fieldName: string): Array<RecordProxy<T> | null> | null;

Как видите, этот метод возвращает массив вместо одного RecordProxy. Как и getRootField, этот метод также ограничен документом GraphQL, созданным соответствующей операцией.

getRootField не может получить несколько записей. Для таких случаев у нас есть метод getPluralRootField. Поэтому, если операция возвращает коллекцию объектов GraphQL вместо одного объекта, мы используем этот метод. Что, если операция вернула Scalar или массив Scalars? В этом случае они не будут сохранены в хранилище, поскольку полезная нагрузка операции не подчиняется глобальной идентификации объекта. Двигаясь дальше, рассмотрим мутацию generateQuestions:

mutation GenerateQuestionsMutation{
# generate n random questions
# generateQuestions(n: Int!): [Question!]!
  generateQuestions(n: 10){
    id
    description
  }
}

Это приводит к следующему документу GraphQL:

generateQuestions(n: 10){
  # Returns 10 randomly generated questions
  id
  description
}

Если я попытаюсь использовать store.getRootField("generateUsers"), это не сработает, поскольку generateQuestions возвращает массив Users. Вместо этого мне нужно сделать

const payload = store.getRootPluralField("generateQuestions");

Полезная нагрузка будет содержать массив Questions (RecordProxy типа Question). Затем вы можете получить к нему доступ как к обычному массиву

const descriptions = payload.map(question => question.getValue("description"));

3.5 create

Этот метод позволяет вам создать объект в хранилище Relay. Прототипом этого метода является

Docs, Typings: create(dataID: DataID, typeName: string): RecordProxy;

Он возвращает RecordProxy, который можно использовать для управления вновь созданным узлом. Чтобы создать новый User с заданным идентификатором, вы можете сделать

const newUser = store.create("NEWUSERID", "User");
newUser.setValue("name", "Lorem Ipsum")

3.6 delete

Этот метод позволяет удалить запись из хранилища реле. Прототипом этого метода является

Docs, Typings: delete(dataID: DataID): void;

Он удаляет запись с данным идентификатором из магазина. Просто как тот. Либо это? Просто убедитесь, что вы удалили запись отовсюду и что на нее нет ссылок. Если есть ссылка на несуществующую запись, это ошибка. Слишком запутанно? Мое практическое правило - удалять запись напрямую. Если все работает нормально, круто. Если этого не произошло, вы можете найти ссылки и удалить их. Помните, что вы всегда можете осмотреть магазин с помощью Relay DevTools, чтобы узнать, что в магазине и где.

3.7 invalidateStore

На данный момент этот метод не поддерживает набор текста. Я думаю, это все еще продолжается. Согласно документации Relay, прототипом является

Docs: invalidateStore(): void

Я мало работал с этим, поэтому отнеситесь к моим словам с недоверием. При повторном запуске запроса у вас есть возможность использовать данные из магазина вместо того, чтобы получать их снова. Это можно сделать, установив fetchPolicy в QueryRenderer на "store-and-network”. В этом случае, если QueryRenderer размонтируется, а затем снова подключится, он не запустит запрос снова, а вместо этого будет использовать данные из хранилища, при условии, что данные все еще действительны. Проще говоря, если вы повторно выбираете запрос, данные которого уже находятся в магазине, данные будут извлечены из магазина, а не из сети.

Если в какой-то момент выполнения программы вы захотите программно изменить это поведение, вы можете использовать этот метод. Когда вызывается invalidateStore, все запросы, запущенные до признания недействительными, помечаются как устаревшие. В документах Relay упоминается, что состояние запроса проверяется

environment.check(query) === "stale" 

Поскольку все запросы теперь устарели, при проверке они будут обновлены. Это означает, что QueryRenderer не будет использовать какие-либо данные из магазина, а вместо этого будет их повторно загружать.

3.8 Summary

Давайте быстро пройдемся по тому, что мы только что видели. RecordSourceSelectorProxy позволяет вам получить доступ к магазину контролируемым образом. Он предоставляет следующие методы:

  1. get: Получает запись из магазина по ее идентификатору.
  2. getRoot: получает корень магазина. Может использоваться для доступа к любой записи.
  3. getRootField: получает единичное поле из заданного документа GraphQL. Можно получить поле только с именем (без указания переменных поля).
  4. getPluralRootField: Подобно getRootField, но получает поле множественного числа (поле, которое возвращает массив) из данного документа GraphQL.
  5. create: Создает новую запись данного типа и ID.
  6. delete: удаляет запись с заданным идентификатором.
  7. invalidateStore: помечает данные магазина как устаревшие.

4. RecordProxy

А вот здесь и начинается самое интересное. RecordSourceSelectorProxy, как следует из названия, дает вам источник записи. Например,

const root = store.getRoot();
const viewerRecord = store.getRoot().getLinkedRecord("viewer");

Теперь Root - это RecordProxy. RecordProxy интерфейс позволяет просматривать записи и управлять ими. У него много методов, и это может занять немного времени. Готовы увидеть интерфейс?

Примечание. Как было сказано ранее, вам не нужно читать здесь эти интерфейсы. Они упомянуты только для полноты картины. Однако вам следует проанализировать, как используются типы.

Согласно документации Relay, интерфейс приведен ниже:

interface RecordProxy {
   copyFieldsFrom(sourceRecord: RecordProxy): void;
   getDataID(): string;
   getLinkedRecord(name: string, arguments?: ?Object): ?RecordProxy;
   getLinkedRecords(name: string, arguments?: ?Object): ?Array<?RecordProxy>;
   getOrCreateLinkedRecord(
     name: string,
     typeName: string,
     arguments?: ?Object,
   ): RecordProxy;
   getType(): string;
   getValue(name: string, arguments?: ?Object): mixed;
   setLinkedRecord(
     record: RecordProxy,
     name: string,
     arguments?: ?Object,
   ): RecordProxy;
   setLinkedRecords(
     records: Array<?RecordProxy>,
     name: string,
     arguments?: ?Object,
   ): RecordProxy;   
  setValue(value: mixed, name: string, arguments?: ?Object): RecordProxy;
   invalidateRecord(): void;
 }

Согласно наборам, интерфейс имеет следующий вид:

interface RecordProxy<T = {}> {
copyFieldsFrom(source: RecordProxy): void;
getDataID(): DataID;
// If a parent type is provided, provide the child type
getLinkedRecord<K extends keyof T>(name: K, args?: Variables | null): RecordProxy<NonNullable<T[K]>>;
// If a hint is provided, the return value is guaranteed to be the      hint type
getLinkedRecord<H = never>(
  name: string,
  args?: Variables | null,
): [H] extends [never] ? RecordProxy | null : RecordProxy<H>;
getLinkedRecords<K extends keyof T>(
  name: K,
  args?: Variables | null,
): Array<RecordProxy<Unarray<NonNullable<T[K]>>>>;
getLinkedRecords<H = never>(
name: string,
args?: Variables | null,
): [H] extends [never]
  ? RecordProxy[] | null
  : NonNullable<H> extends Array<infer U>
    ? Array<RecordProxy<U>> | (H extends null ? null : never)
    : never;
getOrCreateLinkedRecord(name: string, typeName: string, args?: Variables | null): RecordProxy<T>;
getType(): string;
getValue<K extends keyof T>(name: K, args?: Variables | null): T[K];
getValue(name: string, args?: Variables | null): Primitive | Primitive[];
setLinkedRecord<K extends keyof T>(
  record: RecordProxy<T[K]> | null,
  name: K,
  args?: Variables | null,
): RecordProxy<T>;
setLinkedRecord(record: RecordProxy | null, name: string, args?: Variables | null): RecordProxy;
setLinkedRecords<K extends keyof T>(
  records: Array<RecordProxy<Unarray<T[K]>> | null> | null | undefined,
  name: K,
  args?: Variables | null,
): RecordProxy<T>;
setLinkedRecords(
  records: Array<RecordProxy | null> | null | undefined,
  name: string,
  args?: Variables | null,
): RecordProxy<T>;
setValue<K extends keyof T>(value: T[K], name: K, args?: Variables | null): RecordProxy<T>;
setValue(value: Primitive | Primitive[], name: string, args?: Variables | null): RecordProxy;
}

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

4.1 getDataID

Прототипом этого метода является

Docs: getDataID(): string;
Typings: getDataID(): DataID;

Реле определило тип DataID как string

export type DataID = string;

Этот метод просто позволяет вам получить идентификатор данных данной записи, который Relay использует для идентификации записи. Обычно он совпадает с атрибутом id записи. Но это может быть не всегда, как мы увидим в методах getOrCreateLinkedRecord и copyFieldsFrom.

4.2 getType

Прототипом этого метода является

Docs, Typings: getType(): string;

Этот метод позволяет увидеть тип записи. Опять же, здесь ничего особенного.

const viewerType = store.getRoot().getLinkedRecord("viewer").getType(); 

Когда вы регистрируете значение, вы получаете следующее:

console.log(viewerType);
> "User"

4.3 getValue

Как и метод getRootField на RecordSourceSelectorProxy, этот метод также предоставляет два прототипа:

Docs: getValue(name: string, arguments?: ?Object): mixed
Typings:
1. getValue<K extends keyof T>(name: K, args?: Variables | null): T[K];
2. getValue(name: string, args?: Variables | null): Primitive | Primitive[];

Опять же, дженерики предназначены для обеспечения безопасности типов. Теперь getValue используется для доступа к полям, которые представляют Scalars или их массив. То есть его нельзя использовать, если само поле является объектом GraphQL. Для этого у нас есть getLinkedRecord метод.

Напомним, что Relay определяет примитивные типы как

type Primitive = string | number | boolean | null | undefined;

getValue следует использовать, когда поле возвращает любой из вышеуказанных типов или массив вышеуказанных типов. Кроме того, enums преобразуются в тип string. Итак, все Scalars в конечном итоге сводятся к примитивному типу.

Например, чтобы получить name из viewer, вы можете сделать

const name = store
.getRoot()?
.getLinkedRecord("viewer")?
.getValue("name")

Если вы не понимаете оператор ? (необязательное связывание), он эквивалентен

object?.property === object && object.property

Помните, что мы получили вопрос с идентификатором "QUESTION1"? Получим его описание.

const question = store
.getRoot()
.getLinkedRecord("question", "id:"QUESTION1"})
const desc = question?.getValue(`description(locale: "fr")`)

Слишком неуклюже, не правда ли? К счастью, getValue поддерживает второй аргумент для переменных.

const desc = question?.getValue("description", {locale: "fr"})

4.4 getLinkedRecord

Этот метод также предоставляет два прототипа. Как и getValue, этот метод также позволяет передавать переменные во втором аргументе.

Docs: getLinkedRecord(name: string, arguments?: ?Object): ?RecordProxy
Typings: 
1. // If a parent type is provided, provide the child type
getLinkedRecord<K extends keyof T>(name: K, args?: Variables | null): RecordProxy<NonNullable<T[K]>>;
2. // If a hint is provided, the return value is guaranteed to be the hint type
getLinkedRecord<H = never>(
name: string,
args?: Variables | null,
): [H] extends [never] ? RecordProxy | null : RecordProxy<H>;

Мы знаем, что getValue работает только тогда, когда выбираемое поле является примитивом или массивом примитивных типов. Но что, если это поле типа объекта GraphQL? В этом случае вы можете использовать getLinkedRecord.

Предположим, что complicatedUser - это RecordProxy для ответа на complicatedUser запрос. Чтобы получить имя соответствующего пользователя, вы можете сделать

const complicatedUser = store
.getRoot()
.getLinkedRecord("complicatedUser", {userId: "ANOTHERUSER2"})
const name = complicatedUser?.getLinkedRecord("node")?.getValue("name")

4.5 getLinkedRecords

У этого метода есть следующий прототип:

Docs: getLinkedRecords(name: string, arguments?: ?Object): ?Array<?RecordProxy>
Typings: 
1. getLinkedRecords<K extends keyof T>(
name: K,
args?: Variables | null,
): Array<RecordProxy<Unarray<NonNullable<T[K]>>>>;
2. getLinkedRecords<H = never>(
name: string,
args?: Variables | null,
): [H] extends [never]
? RecordProxy[] | null
: NonNullable<H> extends Array<infer U>
? Array<RecordProxy<U>> | (H extends null ? null : never)
: never;

Мы видели, что для получения типа объекта GraphQL мы используем метод getLinkedRecord. Но если поле возвращает массив объектов GraphQL, вы не можете использовать этот метод. Вместо этого вы используете getLinkedRecords, который возвращает массив RecordProxy.

Представьте себе следующую мутацию:

mutation generateQuestions{
  generateQuestions(n: 10){  
     id
     description
  }
}

После активации мутации вы можете получить вопросы, выполнив:

const users = store
.getRoot()
.getLinkedRecords("generateQuestions", {n: 10});

В качестве альтернативы вы можете сделать:

const users = store.getPluralRootField("generateUsers")

Теперь вы можете перебирать пользователей, чтобы получить RecordProxy для отдельного пользователя.

4.6 getOrCreateLinkedRecord

У этого метода есть следующий прототип:

Docs: getOrCreateLinkedRecord(name: string, typeName: string, arguments?: ?Object)
Typings: getOrCreateLinkedRecord(name: string, typeName: string, args?: Variables | null): RecordProxy<T>;

Этот метод попытается получить указанную запись. Если запись не существует, она будет создана. Рассмотрим действие:

const id = store
.getRoot()
.getOrCreateLinkedRecord("gksj", "User")
.getDataID()

Приведенный выше фрагмент получает запись типа User, которая связана с корнем магазина с помощью поля gksj. Если такой записи не существует, она создается и прикрепляется к полю gksj корня магазина. Наконец, вы получаете идентификатор записи.

Как вы уже догадались, Relay создает пустую запись. Независимо от схемы, все поля вновь созданного users будут иметь значение undefined при доступе, включая поле id. Relay назначит записи идентификатор данных, который можно найти с помощью метода getDataID. Вы должны использовать этот идентификатор для получения записи в будущем. Это пример, в котором DataId и id не совпадают.

Если вам нужно привязать аргументы к полю, метод принимает третий аргумент, который является объектом, соответствующим переменным поля.

4.7 setValue

Relay предоставляет следующие прототипы этого метода:

Docs: setValue(value: mixed, name: string, arguments?: ?Object): RecordProxy
Typings: 
1.setValue<K extends keyof T>(value: T[K], name: K, args?: Variables | null): RecordProxy<T>;
2. setValue(value: Primitive | Primitive[], name: string, args?: Variables | null): RecordProxy;

Он используется для установки значения Scalar (или массива значений Scalar), соответствующего полю записи. Как и getValue, он также принимает аргумент для переменных.

Представьте себе следующий пример:

const updatedViewer = store
.getRoot()
.getLinkedRecord("viewer")
.setValue("Yash", "name")

Этот фрагмент не требует пояснений. Вы получаете root, просматриваете его, чтобы получить viewer, а затем обновляете поле name записи viewer. Этот метод возвращает измененную запись.

4.8 copyFieldsFrom

У этого метода есть следующий прототип:

Docs, Typings: copyFieldsFrom(source: RecordProxy): void;

Этот метод обновит выбранный RecordProxy заданным RecordProxy. Например

const user = store
.getRoot()
.getOrCreateLinkedRecord("gksj", "User")
user.copyFieldsFrom(store.getRoot().getLinkedRecord("viewer"))

Вы получаете пользователя, который связан с корневым хранилищем поgksj полю. Затем вы обновляете его с помощью viewer. В результате все поля user устанавливаются на поля программы просмотра.

Поле id записи такое же, как поле id средства просмотра. Но идентификатор данных (__id), который реле использует для идентификации записи, отличается. Чтобы получить эту запись, вам нужно сделать:

store.get("client:root:gksj")

Пока вы не используете id в своем коде, несколько записей с одинаковыми id полями не вызовут проблемы, поскольку Relay внутренне использует разные DataId для их идентификации. Это еще один пример, когда id и DataId разные.

4.9 набор LinkedRecord

Relay предоставляет следующие прототипы этого метода:

Docs: setLinkedRecord(record: RecordProxy, name: string, arguments?: ?Object)
Typings: 
1. setLinkedRecord<K extends keyof T>(
record: RecordProxy<T[K]> | null,
name: K,
args?: Variables | null,
): RecordProxy<T>;
2. setLinkedRecord(record: RecordProxy | null, name: string, args?: Variables | null): RecordProxy;

Он используется для установки связанной записи, соответствующей полю записи. Подобно getValue и setValue, он также принимает аргумент для переменных, которые могут быть привязаны к полю при доступе к нему.

Представьте себе следующий пример:

const newUser = store.create("RANDOMDATAID", "USER");
const complicatedUser = store
.getRoot()
.getLinkedRecord("complicatedUser", {userId: "ANOTHERUSER2"});
complicatedUser.setLinkedRecord(newUser, "node")

Это изменяет node записи типа complicatedUser на вновь созданного пользователя. Обратите внимание, что ANOTHERUSER2 - это id из User, на который указывает поле node записи complicatedUser(userId: "ANOTHERUSER2"). Это не идентификатор объекта типа complicatedResponse, возвращаемый запросом complicatedUser.

4.10 setLinkedRecords

Relay предоставляет следующие прототипы этого метода:

Docs: setLinkedRecords(records: Array<RecordProxy>, name: string, variables?: ?Object)
Typings: 
1. setLinkedRecords<K extends keyof T>(
records: Array<RecordProxy<Unarray<T[K]>> | null> | null | undefined,
name: K,
args?: Variables | null): RecordProxy<T>;
2. setLinkedRecords(records: Array<RecordProxy | null> | null | undefined,
name: string,
args?: Variables | null): RecordProxy<T>;

Он используется для установки списка связанных записей, соответствующих полю записи. Рассмотрим запрос users. Он возвращает результат с разбивкой на страницы.

query UsersQuery{
  users(first: 10){
    edges{
      node{  
        id
        name
      }
    }
    pageInfo{
      startCursor
      endCursor
      hasNextPage
      hasPreviousPage
    }
  }
}

Теперь я могу удалить User с заданным именем из результата этого запроса. Для этого:

const edges = store
.getRootField("users")?
.getLinkedRecords("edges")
edges.filter(edge => edge.getLinkedRecord("node")?.getValue("name") !== "NAME TO BE REMOVED")
store.getRootField("users").setLinkedRecords(edges, "edges");

Как и setLinkedRecord, третий аргумент этого метода - предоставить переменные для выбранного поля. Есть лучший способ сделать это, как мы увидим в следующем интерфейсе.

4.11 invalidateRecord

Как и invalidateStore, среда выполнения реле не предоставляет никаких типизаций для этого метода. Прототипом этого метода является

invalidateRecord(): void

Этот метод аналогичен invalidateStore. Он используется для признания недействительной данной записи. Любой запрос, который ссылается на эту запись, будет помечен как устаревший. Когда этот запрос проверяется с помощью environment.check(query) === ‘stale’, ему потребуется повторная выборка. Любой другой запрос, который не ссылается на эту запись, будет действителен и не будет повторяться.

4.12 Резюме

Этот раздел был о манипуляциях с RecordProxy. Как вы могли заметить, предыдущий и этот раздел дополняют друг друга. Изучив эти разделы, вы можете уверенно браться за магазин и работать с данными в нем. В этом разделе описаны следующие методы:

  1. getDataId: получить идентификатор данных (используемый для идентификации записи в магазине) для данного RecordProxy
  2. getType: получить тип GraphQL для данного RecordProxy
  3. getValue: доступ к Scalar значениям, соответствующим полю заданного RecordProxy
  4. getLinkedRecord: доступ к полю, отличному от Scalar RecordProxy
  5. getLinkedRecords: доступ к полю данного recordProxy, которое соответствует набору не Scalars
  6. setValue: Установить Scalar значение (я), соответствующее полю данного RecordProxy
  7. setLinkedRecord: установить не Scalar, соответствующий полю RecordProxy
  8. setLinkedRecord: установить набор не Scalars, соответствующий полю RecordProxy
  9. getOrCreateLinkedRecord: Попытайтесь получить доступ к полю, отличному от Scalar, на RecordProxy. Если поле не существует, создает для него запись.
  10. copyFieldsFrom: обновляет данный RecordProxy, заменяя поля полями другого заданногоRecordProxy.
  11. invalidateRecord: помечает запись как недействительную, поэтому запросы, в которых она используется, необходимо повторно обрабатывать, если их данные будут использоваться снова.

5. ConnectionHandler

Этот интерфейс в основном предназначен для управления подключениями разбиения на страницы. Среда выполнения реле предоставляет соответствующий ему модуль

import {ConnectionHandler} from "relay-runtime";

Документы предоставляют следующий интерфейс:

interface ConnectionHandler{
   getConnection(
     record: RecordProxy,
     key: string,
     filters?: ?Object,
   ): ?RecordProxy,
   createEdge(
     store: RecordSourceProxy,
     connection: RecordProxy,
     node: RecordProxy,
     edgeType: string,
   ): RecordProxy,
   insertEdgeBefore(
     connection: RecordProxy,
     newEdge: RecordProxy,
     cursor?: ?string,
   ): void,
   insertEdgeAfter(
     connection: RecordProxy,
     newEdge: RecordProxy,
     cursor?: ?string,
   ): void,
   deleteNode(connection: RecordProxy, nodeID: string): void 
}

Модуль экспортирует следующие методы:

export function createEdge(
store: RecordSourceProxy,
record: RecordProxy,
node: RecordProxy,
edgeType: string,
): RecordProxy;
export function deleteNode(record: RecordProxy, nodeID: DataID): void;
export function getConnection(
record: ReadOnlyRecordProxy,
key: string,
filters?: Variables | null,
): RecordProxy | null | undefined;
export function insertEdgeAfter(record: RecordProxy, newEdge: RecordProxy, cursor?: string | null): void;
export function insertEdgeBefore(record: RecordProxy, newEdge: RecordProxy, cursor?: string | null): void;

Как видите, набивки практически не изменились. Так что я обращусь непосредственно к наборам из документации.

5.1 getConnection

Метод имеет следующий прототип

getConnection(record: RecordProxy, key: string, filters?: ?Object)

Рассмотрим следующий запрос:

fragment UserList on Query{
  users(first: 10, orderBy: "name") @connection(
    key: "UserList_users"
  ){
      edges{
        node{  
          id
          name
        }
      }
      pageInfo{
        startCursor
        endCursor
        hasNextPage
        hasPreviousPage
      }
    }
  }
}
query UserListQuery{
  ...UserList_users
}

Чтобы получить доступ к записям, вы можете сделать

const users = store
.getRoot().getLinkedRecord("users", {first: 10, orderBy: "name"})

Но ConnectionHandler предлагает лучший способ через getConnection.

const users = ConnectionHandler.getConnection(
// parent record
store.getRoot(),
// connection key
'UserList_users',
// 'filters' :identify the connection
{orderby: 'name'}
);

Это обеспечивает доступ к соединениям естественным для Relay способом - с помощью ключа и фильтров. Теперь, когда у вас есть пользователи, вы можете

const edges = users.getLinkedRecords("edges")

5.2 deleteNode

Прототип этого метода:

deleteNode(connection: RecordProxy, nodeID: string): void

Он берет заданное соединение и удаляет из него узел с заданным идентификатором. Рассмотрим users соединение из getConnection примера. Чтобы удалить данного пользователя с данным идентификатором, вы можете сделать:

ConnectionHandler.deleteNode(users, 'IDTOBEDELETED')

5.3 createEdge

Прототип этого метода:

createEdge(store: RecordSourceProxy, connection: RecordProxy, node: RecordProxy, edgeType: string)

Этот метод создает границу данного типа для данного соединения. Рассмотрим users соединение из getConnection примера. Чтобы создать край для этого соединения,

//Create a user type object
const user = store.create("NEWUSERID", "USER");
//Populate the fields
user.setValue("RANDOMNAME", "name");
//Create an edge that can be inserted into the users connection
const edge = ConnectionHandler.createEdge(store, users, user, 'UserEdge');

Метод требует RecordSourceProxy для доступа к магазину. Также требуется соединение, в которое должно быть вставлено ребро, узел, который должен содержаться на ребре, и тип ребра GraphQL.

Обратите внимание, что при создании кромки кромка не вставляется в соединение. Это просто дает вам край, готовый для вставки в соединение. Чтобы вставить ребро, у нас есть следующие два метода

5.4 insertEdgeBefore

Прототип этого метода:

insertEdgeBefore(connection: RecordProxy, newEdge: RecordProxy, cursor?: string)

Этот метод вставляет край заданного типа в заданное соединение. Рассмотрим соединение users из примера getConnection и edge, созданное в примере createEdge. Этот метод принимает третий аргумент для cursor. edge вставляется перед этим cursor. Если аргумент cursor не указан, он вставляет edge в начало соединения.

// Insert at the beginning
ConnectionHandler.insertEdgeBefore(users, edge)
// Insert after a cursor
ConnectionHandler.insertEdgeAfter(users, edge, 'MyCursor')

5.5 insertEdgeAfter

Прототип этого метода:

insertEdgeAfter(connection: RecordProxy, newEdge: RecordProxy, cursor?: string)

Этот метод также вставляет край заданного типа в заданное соединение. Рассмотрим соединение users из примера getConnection и edge, созданное в примере createEdge. Этот метод принимает третий аргумент для cursor. После этого cursor вставляется edge. Если аргумент cursor не указан, он вставляет edge в конец соединения.

// Insert at the end
ConnectionHandler.insertEdgeAfter(users, edge)
// Insert after a cursor
ConnectionHandler.insertEdgeAfter(users, edge, 'MyCursor')

5.6 Резюме

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

  1. getConnection: получить соединение с заданными параметрами из магазина
  2. deleteNode: удалить узел из данного соединения
  3. createEdge: создать ребро, которое можно вставить в данное соединение
  4. insertEdgeBefore: вставляет ребро в начало или перед указанным курсором
  5. insertEdgeAfter: вставляет ребро в конец или после данного курсора

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

Подведение итогов

Хотя эта статья могла показаться бесконечной, у нее есть конец. Это хорошая вещь? Это плохо? Я оставляю это на ваше усмотрение. Эта статья пытается охватить большую часть материала. Но есть некоторые вещи, которые вы узнаете, используя Relay, и включать их здесь нецелесообразно и нецелесообразно. Внутренняя работа магазина со временем может измениться, но методы должны оставаться прежними - более или менее. Я постараюсь обновлять эту статью, пока не улучшится документация по Relay. Позже я также планирую добавить раздел распространенные ошибки.

Эта статья вам помогла? Он что-то упустил? Можно ли немного обрезать? Как могло быть лучше? Позвольте мне знать в комментариях ниже : )