Забавно, но с распространением интернета в мире все больше востребованы веб-приложения, работающие без подключения к сети. Пользователи (и клиенты) хотят функциональные интернет-приложения в онлайн, офлайн и в зонах с неустойчивой связью.
А это… не просто.
Посмотрим, как создать эффективное решение, работающее без подключения к сети, на React и слое данных GraphQL с применением Apollo Client. Статья разбита на две части. На этой неделе разберем оффлайновые запросы. На следующей неделе примемся за мутации.
Redux Persist и Redux Offline
За спиной Apollo Client все тот же Redux. А это значит, вся экосистема Redux с многочисленными инструментами и библиотеками доступна в приложениях на Apollo.
В сфере поддержки офлайн у Redux два основных игрока: Redux Persist и Redux Offline.
Redux Persist прекрасный, но дающий лишь основу, инструмент для загрузки хранилища redux в localStorage
и восстановления обратно (поддерживаются и другие места хранения). По принятой терминологии, восстановление также называется регидрацией.
Redux Offline расширяет возможности Redux Persist дополнительным слоем функций и утилит. Redux Offline автоматически узнает о разрывах и восстановлениях связи и при разрыве позволяет ставить в очередь действия и операции, а при восстановлении – автоматически воспроизводит эти действия вновь.
Redux Offline – это «заряженый» вариант для работы без сетевого подключения.
Запросы в офлайн
Apollo Client и сам неплохо справляется с краткосрочными перебоями сетевых подключений. Когда сделан запрос, его результат помещается в собственное хранилище Apollo.
Если тот же запрос делается повторно, результат немедленно берется из хранилища на клиенте и отдается запрашивающему компоненту, кроме случая, когда параметр fetchPolicy имеет значение network-only. Это значит, что при отсутствии сетевого подключения или недоступности сервера повторные запросы будут возвращать последний сохраненный результат.
Однако стоит пользователю закрыть приложение, и хранилище пропадет. Как не потерять хранилище Apollo на клиенте между перезапусками приложения?
На помощь приходит Redux Offline.
Apollo держит свои данные в хранилище Redux (в ключе apollo
). Записывая хранилище целиком в localStorage
и восстанавливая при следующем запуске приложения, можно переносить результаты прошлых запросов между сеансами работы с приложением даже при отсутствии подключения к интернету.
Использование 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 признал проблему и предложил рецепт отложенного рендеринга приложения после восстановления. К сожалению, его решение основано на ручном вызове 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 заявлены исправления некоторых странных проявлений при работе с Redux Offline.
Другая странность этой пары в том, что Redux Offline, кажется, не слишком хорошо уживается с Apollo Client Devtools. Попытки использовать Redux Offline с установленным Devtools иногда приводят к неожиданным и, казалось бы, бессвязным ошибкам.
Такие ошибки можно легко исключить, если при создании Apollo client
не подключать его к Devtools.
export const client = new ApolloClient({
networkInterface,
connectToDevTools: false
});
Будьте на связи
Redux Offline дает приложению на Apollo основые механизмы для выполнения запросов даже при перезагрузке приложения после отключения от сервера.
Через неделю погрузимся в обработку офлайновых мутаций с помощью Redux Offline.
Будьте на связи!
Перевод статьи. Автор оригинала Pete Corey.
miooim
Спасибо, очень полезная статья.