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

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

В этой статье мы рассмотрим несколько примеров простого приложения Next.js, которое использует Apollo для извлечения данных из еще более простой серверной части graphQL Yoga / Prisma. Если вы хотите следовать этим примерам, вы можете скачать стартовые файлы здесь, в главной ветке: «https://github.com/martinseanhunt/padgey-nation-fronten lived

Этот интерфейс связан с демо-сервером, который размещен на now.sh, поэтому вы должны просто иметь возможность запускать npm install и npm run dev, и все будет в порядке!

Эта проблема

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

Однако изначально, когда мы вносим изменения в данные на нашем сервере с помощью мутации graphQL, это изменение не будет отражено в состоянии приложения, если мы не обновим страницу.

refetchQueries

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

readQuery и writeQuery

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

Например, вот проблема, с которой мы можем столкнуться при удалении элемента из кеша вручную с помощью функции обновления в нашем Create.js компоненте:

Вот наглядный пример одной из проблем, которые это может создать:

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

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

Если вы хотите изучить кодовую базу для этого подхода, вы можете найти ее здесь: https://github.com/martinseanhunt/padgey-nation-frontend/tree/update-cache-directly

fetchPolicy

Другое возможное решение - установить fetchPolicy наших начальных запросов равным ‘network-only’. Это означает, что данные для текущей страницы всегда будут поступать с сервера. Этот подход работает отлично, однако вы теряете мгновенное ощущение, которое возникает при обслуживании кэшированных данных, когда они доступны, и на сервер будет отправлено больше запросов, чем необходимо.

<Query
  query={GET_LIST_ITEMS_QUERY}
  fetchPolicy='network-only'
  variables={{ skip: (page - 1) * perPage }}
>

Куда мы отправимся отсюда ?

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

Временное рабочее решение…

К счастью, есть очень простой обходной путь, который должен работать в большинстве случаев . Когда у нас есть доступ к объекту кеша, мы можем вызвать cache.data.delete(key), где key - это ключ, который Apollo использует для хранения данных для определенного элемента. И запись будет полностью удалена из кеша.

Вы можете видеть, что Apollo берет typename нашего конкретного фрагмента данных и добавляет уникальный идентификатор для создания key.

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

Чтобы это работало в нашем приложении, мы можем сделать следующее:

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

2) Обновите нашу Мутацию в Create.js, чтобы она вызывала нашу новую вспомогательную функцию при обновлении при создании нового элемента списка. (не забудьте также повторно загрузить запрос на подключение)

3) В Index.js деструктурируйте refetch из объекта, предоставленного для рендеринга функции prop в нашем запросе, и передайте его в ListItem.js.

Мы передаем эту функцию повторной выборки в ListItems.js, чтобы мы могли вызывать повторную выборку текущей страницы при удалении элемента. В противном случае в текущей версии Apollo компонент Index.js не будет повторно отрисован, и запрос не будет повторно запущен. Я считаю, что причина, по которой использование cache.data.delete не запускает повторную визуализацию родительских компонентов, заключается в том, что мы напрямую изменяем объект данных кеша, но в этом я не уверен на 100%.

4) Создайте и вызовите нашу функцию обновления в ListItems.js, когда listItem будет удален (снова не забудьте повторно загрузить наш запрос на подключение к странице).

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

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

Полная кодовая база для этого решения доступна здесь: https://github.com/martinseanhunt/padgey-nation-frontend/tree/simple-solution

Вы можете увидеть полную разницу между стартовыми файлами и рабочим решением здесь: https://github.com/martinseanhunt/padgey-nation-frontend/compare/master...simple-solution

А живую демонстрацию вы можете увидеть на https://padgey-nation.now.sh

5) Недостатки этого временного решения

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

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

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

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

Я знаю еще один способ

Я столкнулся с вышеуказанным обходным путем, когда кто-то упомянул об этом несколько дней назад в проблеме с github, которую я отслеживал (https://github.com/apollographql/apollo-feature-requests/issues/4#issuecomment-431119231 ). В целом, я думаю, что на данный момент это более простое и элегантное решение. Однако в тот момент, когда я столкнулся с вышеуказанным методом, я уже работал над решением, которое не требует сопоставления всего кеша и не изменяет напрямую объект кеша. Также этот метод БУДЕТ работать, если у вас есть элементы с таким же именем в других частях приложения, которые не нужно обновлять.

Однако это решение немного сложнее по своей природе, и я считаю, что его еще можно улучшить.

Общий обзор идеи состоит в том, что мы сохраняем в локальном состоянии массив номеров страниц, которые необходимо обновить, и в зависимости от того, находится ли текущая запрашиваемая страница в массиве, мы либо отправляем запрос с fetchPolicy из ‘network-only’, либо с ‘cache-first’

Вот две основные функции, задействованные в этой работе:

1) setPagesToBeRefreshed - это вызывается при обновлении нашими мутациями, ответственными за создание и удаление элементов списка

2) getItemsForPage - это вызывается в различных точках жизненного цикла Index.js, чтобы решить, искать ли наши данные в кеше или идти прямо на сервер.

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

Если вам интересно, вы можете изучить полную базу кода здесь: https://github.com/martinseanhunt/padgey-nation-frontend/tree/refetch-queries-only-when-needed

Разницу между начальными файлами и конечным продуктом можно увидеть здесь: https://github.com/martinseanhunt/padgey-nation-frontend/compare/master...refetch-queries-only-when-needed

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

Разобраться во всем этом было настоящим путешествием. Я все еще новичок в экосистеме graphQL, но очень рад увидеть, куда нас приведут эти технологии. Если у кого-то есть конструктивная критика или отзыв об этой статье, я буду рад это услышать.

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

Удачного кодирования!