February 27, 2019

React-apollo - нишевое решение или универсальная таблетка?

Apollo сделали много классных инструментов и технологий для работы в связке с gprahql. Конечно же они не могли пройти мимо хайпового react и привнесли свой инстурмент и в эту экосистему и назвали его react-apollo. Давайте посмотрим что именно предлагает их инстурмент:

Query component

Центральная фича react-apollo это компонет <Query />. Он позволяет связать ваш компонент напрямую с graphQL запросом наприямую. Вот как это выглядит:

const GET_DOGS = gql`
  query GetDogs {
    dogs {
      id
      name
    }
  }
`;

const GoodDogsBrent = () => (
  <Query query={GET_DOGS}>
    {({ loading, error, data }) => {
      if (error) return <Error />
      if (loading || !data) return <Fetching />

      return <DogList dogs={data.dogs} />
    }}
  </Query>
)

У нас есть квери GET_DOGS которая просто передается компоненту, далее он берет всю работу на себя - сообщаяет нам о состоянии загрузки, ошибках, и в конце отдает нам данные. Т.е. автоматически поставляет нашему компоненту три локальных состояния. Почему эту круто я думаю обьяснять не нужно - мы только что описали логику "сходи-забери-отобрази" в 17 строчек кода с прелоадером и отображением ошибки.

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

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

const GET_DOGS = gql`
  query GetDogs {
    dogs {
      id
      name
    }
  }
`;

const GoodDogsBrent = () => (
  <Async promise={client.qeury(GET_DOGS)}>
    {({ loading, error, data }) => {
      if (error) return <Error />
      if (loading || !data) return <Fetching />

      return <DogList dogs={data.dogs} />
    }}
  </Async>
)

Такой комонент пишется за час-два зависимости от вашего опыта работы с реактом, вот пример реализации в 26 строк форматированого кода. Он работает с любым промисом и никак не привязан к graphQL.

Круто, так давайте все так писать, это же намного проще чем FLUX

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

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

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

Но что если в середине проекта выяснится что наш католог стал магазином и теперь мне нужна корзина для покупок доступная везде?

К сожалению здесь красивая история заканчивается и начинается, что называется - не очень. Кодовая база уже написана а переписывать все не вариант (а так как вся логика хранится у нас прямо в верстке, и не выделена в отдельный слой переписывать придется действительно много). Так появились народные решения "react-redux поверх react-apollo" - когда каждый компонент может быть подконекчен к стору редакса и к аполо клиенту одновременно. Это выход из ситуации, но он дает дополнительную серию перезапусков рендер функций так как теперь у нас два источника состояния данных на которые нужно реактивно отреагировать (т.е. приложение полуается завернутым в <Provider> реакт-редакса и <AppoloProvider> реакт-аполло).

Для выхода из этой не очень красивой ситуации аполо предлогает следующее:

"Мы уже используем хранилище состояния внутри нашего клиента для реализации кэша и механизмов оптимизации запросов на бекенд - вот вам api от нашего кэша, ложите ваше состояние туда и проблема решена". И назвали это apollo-link-state.

Что вобще может пойти не так с apollo-link-state

Конечно же там не редакс и экшены с редьюсерами вам писать не придется. Вам придется делать кое-что другое - вам придется писать graphql бекенд приямо у вас на фронтенде для того чтобы работать с их кешем. Весь фокус заключается в том, что теперь если поменить квери меткой @client то аплоло не будет отправлять этот запрос на бекенд а поищет резолверы на фронтенде для этой квери. Предлагаемое решение очень напоминает костыль:

Из клиентской квери вы можете выполнять другие клиенсткие или не клиентские квери. Однако не следует думать что это аналог редьюсера - вы не возвращаете новое состояние, вы просто напрямую пишете данные в кеш (write.cache) или пишите их маскируя под другую квери (write.query) или просто возвращаете из резолвера результат и он пишется как кэш клиентской квери, или не пишется в зависимости что вы указали в компонете <Query> через параметр cache policy. (Я даже не представляю что будет если для клиентской квери я выставлю network-only 🤯).

Так как вы залезли на територию graphql - все ваши данные на всех уровнях вложенности должны получить тип (__typename), иначе механизмы кэширования аппло расчитаные на graphql ломаются и начинают спамить в консоль об этом без умолку.

Так как это graphql экшенов у вас тоже нет. У вас есть мутации квери и подписки. Мутации меняют кэш а данные просто получают данные оттуда? Но постойте - квери же кэшируются апло - значит квери тоже мутируют стейт. Тогда в чем их отличие от мутаций? И если мы реактивно реагируем на изменения кэша после того как квери разрезолвится зачем нам подписки?

Разумеется ни о каком FLUX или однонаправленном потоке данных речи тут не идет. Мы начинаем ковырятся во внутрянке реализации apollo-client отчаянно пытаясь пофиксить отсуствие flux в apollo-react. Тулы для дебагинга и просмотра кэша на данный момент сломаны что тоже не добавляет оптимизма. Как будто этого мало - в виду зелености технологии информации очень мало даже в собственной документации apollo. Количество велосипедов стремительно ростет. Но решение есть.

Просто не используйте apollo-react там где нужен FLUX

Вам не нужно отказываться от apollo-client и его замечательных возможностей, вы по прежнему можете использовать graphql, но нужно отказатся от соблазна писать логику работы прямо в JSX шаблонах если вам нужно что-то сложнее получил-показал или такой функционал предвитеться в будущем.