Mirror: The highly customizable and versatile GraphQL client with which you add on features like normalized caching as you grow.
1--- 2title: Offline Support 3order: 7 4--- 5 6# Offline Support 7 8_Graphcache_ allows you to build an offline-first app with built-in offline and persistence support, 9by adding a `storage` interface. In combination with its [Schema 10Awareness](./schema-awareness.md) support and [Optimistic 11Updates](./cache-updates.md#optimistic-updates) this can be used to build an application that 12serves cached data entirely from memory when a user's device is offline and still display 13optimistically executed mutations. 14 15## Setup 16 17Everything that's needed to set up offline-support is already packaged in the 18`@urql/exchange-graphcache` package. 19 20We initially recommend setting up the [Schema Awareness](./schema-awareness.md). This adds our 21server-side schema information to the cache, which allows it to make decisions on what partial data 22complies with the schema. This is useful since the offline cache may often be lacking some data but 23may then be used to display the partial data we do have, as long as missing data is actually marked 24as optional in the schema. 25 26Furthermore, if we have any mutations that the user doesn't interact with after triggering them (for 27instance, "liking a post"), we can set up [Optimistic 28Updates](./cache-updates.md#optimistic-updates) for these mutations, which allows them to be 29reflected in our UI before sending a request to the API. 30 31To actually now set up offline support, we'll swap out the `cacheExchange` with the 32`offlineExchange` that's also exported by `@urql/exchange-graphcache`. 33 34```js 35import { Client, fetchExchange } from 'urql'; 36import { offlineExchange } from '@urql/exchange-graphcache'; 37 38const cache = offlineExchange({ 39 schema, 40 updates: { 41 /* ... */ 42 }, 43 optimistic: { 44 /* ... */ 45 }, 46}); 47 48const client = new Client({ 49 url: 'http://localhost:3000/graphql', 50 exchanges: [cache, fetchExchange], 51}); 52``` 53 54This activates offline support, however we'll also need to provide the `storage` option to the 55`offlineExchange`. The `storage` is an adapter that contains methods for storing cache data in a 56persisted storage interface on the user's device. 57 58By default, we can use the default storage option that `@urql/exchange-graphcache` comes with. This 59default storage uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) to 60persist the cache's data. We can use this default storage by importing the `makeDefaultStorage` 61function from `@urql/exchange-graphcache/default-storage`. 62 63```js 64import { Client, fetchExchange } from 'urql'; 65import { offlineExchange } from '@urql/exchange-graphcache'; 66import { makeDefaultStorage } from '@urql/exchange-graphcache/default-storage'; 67 68const storage = makeDefaultStorage({ 69 idbName: 'graphcache-v3', // The name of the IndexedDB database 70 maxAge: 7, // The maximum age of the persisted data in days 71}); 72 73const cache = offlineExchange({ 74 schema, 75 storage, 76 updates: { 77 /* ... */ 78 }, 79 optimistic: { 80 /* ... */ 81 }, 82}); 83 84const client = new Client({ 85 url: 'http://localhost:3000/graphql', 86 exchanges: [cache, fetchExchange], 87}); 88``` 89 90## React Native 91 92For React Native, we can use the async storage package `@urql/storage-rn`. 93 94Before installing the [library](https://github.com/urql-graphql/urql/tree/main/packages/storage-rn), ensure you have installed the necessary peer dependencies: 95 96- NetInfo ([RN](https://github.com/react-native-netinfo/react-native-netinfo) | [Expo](https://docs.expo.dev/versions/latest/sdk/netinfo/)) and 97- AsyncStorage ([RN](https://react-native-async-storage.github.io/async-storage/docs/install) | [Expo](https://docs.expo.dev/versions/v42.0.0/sdk/async-storage/)). 98 99```sh 100yarn add @urql/storage-rn 101# or 102npm install --save @urql/storage-rn 103``` 104 105You can then create the custom storage and use it in the offline exchange: 106 107```js 108import { makeAsyncStorage } from '@urql/storage-rn'; 109 110const storage = makeAsyncStorage({ 111 dataKey: 'graphcache-data', // The AsyncStorage key used for the data (defaults to graphcache-data) 112 metadataKey: 'graphcache-metadata', // The AsyncStorage key used for the metadata (defaults to graphcache-metadata) 113 maxAge: 7, // How long to persist the data in storage (defaults to 7 days) 114}); 115``` 116 117## Offline Behavior 118 119_Graphcache_ applies several mechanisms that improve the consistency of the cache and how it behaves 120when it's used in highly cached-dependent scenarios, including when it's used with its offline 121support. We've previously read about some of these guarantees on the ["Normalized Caching" 122page.](./normalized-caching.md) 123 124While the client is offline, _Graphcache_ will also apply some opinionated mechanisms to queries and 125mutations. 126 127When a query fails with a Network Error, which indicates that the client is 128offline the `offlineExchange` won't deliver the error for this query to avoid it from being 129surfaced to the user. This works particularly well in combination with ["Schema 130Awareness"](./schema-awareness.md) which will deliver as much of a partial query result as possible. 131In combination with the [`cache-and-network` request policy](../basics/document-caching.md#request-policies) 132we can now ensure that we display as much data as possible when the user is offline while still 133keeping the cache up-to-date when the user is online. 134 135A similar mechanism is applied to optimistic mutations when the user is offline. Normal 136non-optimistic mutations are executed as usual and may fail with a network error. Optimistic 137mutations however will be queued up and may be retried when the app is restarted or when the user 138comes back online. 139 140If we wish to customize when an operation result from the API is deemed an operation that has failed 141because the device is offline, we can pass a custom `isOfflineError` function to the 142`offlineExchange`, like so: 143 144```js 145const cache = offlineExchange({ 146 isOfflineError(error, _result) { 147 return !!error.networkError; 148 }, 149 // ... 150}); 151``` 152 153However, this is optional, and the default function checks for common offline error messages and 154checks `navigator.onLine` for you. 155 156## Custom Storages 157 158In the [Setup section](#setup) we've learned how to use the default storage engine to store 159persisted cache data in IndexedDB. You can also write custom storage engines, if the default one 160doesn't align with your expectations or requirements. 161One limitation of our default storage engine is for instance that data is stored time limited with a 162maximum age, which prevents the database from becoming too full, but a custom storage engine may 163have different strategies for dealing with this. 164 165[The API docs list the entire interface for the `storage` option.](../api/graphcache.md#storage-option) 166There we can see the methods we need to implement to implement a custom storage engine. 167 168Following is an example of the simplest possible storage engine, which uses the browser's 169[Local Storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage). 170Initially we'll implement the basic persistence methods, `readData` and `writeData`. 171 172```js 173const makeLocalStorage = () => { 174 const cache = {}; 175 176 return { 177 writeData(delta) { 178 return Promise.resolve().then(() => { 179 Object.assign(cache, delta); 180 localStorage.setItem('data', JSON.stringify(cache)); 181 }); 182 }, 183 readData() { 184 return Promise.resolve().then(() => { 185 const local = localStorage.getItem('data') || null; 186 Object.assign(cache, JSON.parse(local)); 187 return cache; 188 }); 189 }, 190 }; 191}; 192``` 193 194As we can see, the `writeData` method only sends us "deltas", partial objects that only describe 195updated cache data rather than all cache data. The implementation of `writeMetadata` and 196`readMetadata` will however be even simpler, since it always sends us complete data. 197 198```js 199const makeLocalStorage = () => { 200 return { 201 /* ... */ 202 writeMetadata(data) { 203 localStorage.setItem('metadata', JSON.stringify(data)); 204 }, 205 readMetadata() { 206 return Promise.resolve().then(() => { 207 const metadataJson = localStorage.getItem('metadata') || null; 208 return JSON.parse(metadataJson); 209 }); 210 }, 211 }; 212}; 213``` 214 215Lastly, the `onOnline` method will likely always look the same, as long as your `storage` is 216intended to work for browsers only: 217 218```js 219const makeLocalStorage = () => { 220 return { 221 /* ... */ 222 onOnline(cb: () => void) { 223 window.addEventListener('online', () => { 224 cb(); 225 }); 226 }, 227 }; 228}; 229```