- PVSM.RU - https://www.pvsm.ru -

Запросы GraphQL без подключения к сети с помощью Redux Offline и Apollo

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

А это… не просто.

Посмотрим, как создать эффективное решение, работающее без подключения к сети, на React [1] и слое данных GraphQL [2] с применением Apollo Client [3]. Статья разбита на две части. На этой неделе разберем оффлайновые запросы. На следующей неделе примемся за мутации.

Redux Persist и Redux Offline

За спиной Apollo Client [4] все тот же Redux [5]. А это значит, вся экосистема Redux с многочисленными инструментами и библиотеками доступна в приложениях на Apollo.

В сфере поддержки офлайн у Redux два основных игрока: Redux Persist и Redux Offline.

Redux Persist [6] прекрасный, но дающий лишь основу, инструмент для загрузки хранилища redux в localStorage и восстановления обратно (поддерживаются и другие места хранения [7]). По принятой терминологии, восстановление также называется регидрацией.

Redux Offline [8] расширяет возможности Redux Persist дополнительным слоем функций и утилит. Redux Offline автоматически узнает о разрывах и восстановлениях связи и при разрыве позволяет ставить в очередь действия и операции, а при восстановлении – автоматически воспроизводит эти действия вновь.

Redux Offline – это «заряженый» вариант для работы без сетевого подключения.

Запросы в офлайн

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

Если тот же запрос делается повторно, результат немедленно берется из хранилища на клиенте и отдается запрашивающему компоненту, кроме случая, когда параметр fetchPolicy имеет значение network-only [9]. Это значит, что при отсутствии сетевого подключения или недоступности сервера повторные запросы будут возвращать последний сохраненный результат.

Однако стоит пользователю закрыть приложение, и хранилище пропадет. Как не потерять хранилище Apollo на клиенте между перезапусками приложения?

На помощь приходит Redux Offline.

Apollo держит свои данные в хранилище Redux (в ключе apollo). Записывая хранилище целиком в localStorage и восстанавливая при следующем запуске приложения, можно переносить результаты прошлых запросов между сеансами работы с приложением даже при отсутствии подключения к интернету [10].

Использование Redux Offline и Apollo Client не обходится без нюансов. Посмотрим, как заставить работать вместе обе библиотеки.

Создание хранилища вручную

Обычно клиент Apollo создается довольно просто:

export const client = new ApolloClient({
    networkInterface
});

Конструктор ApolloClient автоматически создает хранилище Apollo (и косвенно – хранилище Redux). Полученный экземпляр client подается в компонент ApolloProvider:

ReactDOM.render(
    <ApolloProvider client={client}>
        <App />
    </ApolloProvider>,
    document.getElementById('root')
);

При использовании Redux Offline необходимо вручную создавать хранилище Redux. Это позволяет подключить к хранилищу промежуточный обработчик (middleware) из Redux Offline. Для начала просто повторим то, что делает сам Apollo:

export const store = createStore(
    combineReducers({ apollo: client.reducer() }),
    undefined,
    applyMiddleware(client.middleware())
);

Здесь хранилище store использует редьюсер и промежуточный обработчик (middleware) из экземпляра Apollo (переменная client), а в качестве исходного состояния указано undefined.

Теперь можно подать store в компонент ApolloProvider:

<ApolloProvider client={client} store={store}>
    <App />
</ApolloProvider>

Превосходно. Создание хранилища Redux под контролем, и можно продолжать с Redux Offline.

Основы персистентного хранения запросов

В простейшем случае добавление Redux Offline заключается в добавлении еще одного промежуточного обработчика к хранилищу.

import { offline } from 'redux-offline';
import config from 'redux-offline/lib/defaults';
export const store = createStore(
    ...
    compose(
        applyMiddleware(client.middleware()),
        offline(config)
    )
);

Без дополнительных настроек обработчик offline начнет автоматически записывать хранилище Redux в localStorage.

Не верите?

Откройте консоль и получите из localStorage эту запись:

localStorage.getItem("reduxPersist:apollo");

Выводится большой объект JSON, представляющий полное текущее состояние приложения Apollo.

Великолепно!

Теперь Redux Offline автоматически делает снимки хранилища Redux в записывает их в localStorage. При каждом запуске приложения сохраненное состояние автоматически берется из localStorage и восстанавливается в хранилище Redux.

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

Конкуренция при восстановлении хранилища

Увы, восстановление хранилища происходит не мгновенно. Если приложение делает запрос в то время, как Redux Offline восстанавливает хранилище, могут происходить Странные Вещи(tm).

Если в Redux Offline включить логирование для режима autoRehydrate (что само по себе заставляет понервничать), при первоначальной загрузке приложения можно увидеть ошибки, на подобии:

21 actions were fired before rehydration completed. This can be a symptom of a race condition where the rehydrate action may overwrite the previously affected state. Consider running these actions after rehydration: …
Выполнено 21 действие прежде чем завершилось восстановление. Это возможный признак конкуренции, из-за чего при восстановлении может быть потеряно ранее настроенное состояние. Рассмотрите возможность выполнять эти действия после восстановления: …

Разработчик Redux Persist признал проблему и предложил рецепт отложенного рендеринга приложения [11] после восстановления. К сожалению, его решение основано на ручном вызове persistStore. Однако Redux Offline делает такой вызов автоматически.

Посмотрим на другое решение.

Создадим Redux action с названием REHYDRATE_STORE, а также соответствующий редьюсер, устанавливающий значение true для признака rehydrated в хранилище Redux:

export const REHYDRATE_STORE = 'REHYDRATE_STORE';

export default (state = false, action) => {
    switch (action.type) {
        case REHYDRATE_STORE:
            return true;
        default:
            return state;
    }
};

Подключим созданный редьюсер к хранилищу и настроим Redux Offline так, чтобы по окончанию восстановления выполнялось новое действие.

export const store = createStore(
    combineReducers({
        rehydrate: RehydrateReducer,
        apollo: client.reducer()
    }),
    ...,
    compose(
        ...
        offline({
            ...config,
            persistCallback: () => {
                store.dispatch({ type: REHYDRATE_STORE });
            },
            persistOptions: {
                blacklist: ['rehydrate']
            }
        })
    )
);

Превосходно! Когда Redux Offline восстанавит хранилище, то вызовет функцию persistCallback, которая запустит действие REHYDRATE_STORE и в конечном счете установит признак rehydrate в хранилище.

Добавление rehydrate в массив blacklist гарантирует, что эта часть хранилища не будет записана в localStorage и восстановлена из него.

Теперь, когда в хранилище есть сведения об окончании восстановления, разработаем компонент, реагирующий на изменения поля rehydrate и визуализирующий дочерние компоненты, только если rehydrate равно true:

class Rehydrated extends Component {
    render() {
        return (
            <div className="rehydrated">
                {this.props.rehydrated ? this.props.children : <Loader />}
            </div>
        );
    }
}

export default connect(state => {
    return {
        rehydrate: state.rehydrate
    };
})(Rehydrate);

Наконец, поместим компонент <App /> внутрь компонента <Rehydrated />, чтобы предотвратить вывод приложения до окончания регидрации:

<ApolloProvider client={client} store={store}>
    <Rehydrated>
        <App />
    </Rehydrated>
</ApolloProvider>

Уфф.

Теперь приложение будет беспечно ждать, пока Redux Offline восстановит хранилище из localStorage, и только потом продолжит отрисовку и будет делать все последующие запросы или мутации GraphQL.

Странности и пояснения

Есть несколько странностей и требующих пояснения вещей при использовании Redux Offline вместе с клиентом Apollo.

Во-первых, надо заметить, что в примерах этой статьи используется версия 1.9.0-0 пакета apollo-client. Для Apollo Client версии 1.9 [12] заявлены исправления некоторых странных проявлений при работе с Redux Offline [13].

Другая странность этой пары в том, что Redux Offline, кажется, не слишком хорошо уживается с Apollo Client Devtools [14]. Попытки использовать Redux Offline с установленным Devtools иногда приводят к неожиданным и, казалось бы, бессвязным ошибкам.

Такие ошибки можно легко исключить, если при создании Apollo client не подключать его к Devtools.

export const client = new ApolloClient({
    networkInterface,
    connectToDevTools: false
});

Будьте на связи

Redux Offline дает приложению на Apollo основые механизмы для выполнения запросов даже при перезагрузке приложения после отключения от сервера.

Через неделю погрузимся в обработку офлайновых мутаций с помощью Redux Offline.

Будьте на связи!

Перевод статьи [15]. Автор оригинала Pete Corey.

Автор: teux

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/api/261955

Ссылки в тексте:

[1] React: https://facebook.github.io/react/

[2] GraphQL: http://graphql.org/

[3] Apollo Client: http://www.apollodata.com/

[4] Apollo Client: https://github.com/apollographql/apollo-client

[5] Redux: http://redux.js.org/

[6] Redux Persist: https://github.com/rt2zz/redux-persist

[7] места хранения: https://github.com/rt2zz/redux-persist#storage-engines

[8] Redux Offline: https://github.com/jevakallio/redux-offline

[9] параметр fetchPolicy имеет значение network-only: http://dev.apollodata.com/react/api-queries.html#graphql-config-options-fetchPolicy

[10] отсутствии подключения к интернету: https://github.com/jevakallio/redux-offline#progressive-web-apps

[11] рецепт отложенного рендеринга приложения: https://github.com/rt2zz/redux-persist/blob/master/docs/recipes.md#delay-render-until-rehydration-complete

[12] версии 1.9: https://github.com/apollographql/apollo-client/blob/df42883c3245ba206ddd72a9cffd9a1522eee51c/CHANGELOG.md#v190-0

[13] странных проявлений при работе с Redux Offline: https://github.com/apollographql/apollo-client/issues/424#issuecomment-316634765

[14] Apollo Client Devtools: http://dev.apollodata.com/core/devtools.html#Apollo-Client-Devtools

[15] статьи: http://www.east5th.co/blog/2017/07/24/offline-graphql-queries-with-redux-offline-and-apollo/

[16] Источник: https://habrahabr.ru/post/335210/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best