At YLD we have been doing some work with offline-first applications and, with the release of Service Workers, a major step has been taken in building these kind of applications in the browser.We have also been using CouchDB and PouchDB to access data while the Application is offline. At some point we started to wonder if we could do the same on native mobile apps while using React Native.
The problemCan we achieve an offline-first experience using React Native? Can we store data locally? Can we sync data between multiple devices?
The solutionFor this purpose, we built a simple collaborative application where users can add or remove notes. You can find it here .
React Native react-native-offline-first |-- index.ios.js |-- DocsApp.js |-- Docs.js |-- DocForm.jsThe entry points for a react native application are index.ios.js and index.android.js , for iOS and Android devices respectively.
However, for this particular example, we focused on iOS only.
DocsApp.js is the main component, also where the main state is kept. Docs.js is the documents list. DocForm.js is used to add new documents. PouchDBLuckily PouchDB exists and we can take advantage of its replication system to store data locally when offline and synchronise it with a remote CouchDB database once the application is back online.
Setup the databasesWe can either create/use a local database, or use PouchDB as a client to a remote CouchDB instance.
const localDB = new PouchDB('docs'); const remoteDB = new PouchDB('http://localhost:5984/docs'); Replicate the data from local store to remote CouchDB and vice-versaThe PouchDB API provides a method for bidirectional data replication. It accepts the live option, so that all changes continue to be replicated, and the retry option, to attempt replications if the application goes offline.
const sync = localDB.sync(remoteDB, { live: true, retry: true }); Persisting the dataThe data is persisted in the local PouchDB and replicated when possible.
onDocSubmit(doc) { localDB.put({_id: doc, content: doc, imageUrl: imageUrl}) .catch(console.log.bind(console, 'Error inserting')); } Subscribe to database changesWe can also subscribe to the changes feed so that after receiving a change ― either from the remote server or the local user ― the UI is updated, either by creating, updating or deleting a document.
class DocsApp extends Component { /* ... */ componentDidMount { localDB.changes({ live: true, include_docs: true //Include all fields in the doc field }).on('change', this.handleChange.bind(this)) } handleChange(change) { var doc = change.doc; if (!doc) { return; } if (doc._deleted) { this.removeDoc(doc); } else { this.addDoc(doc); } } addDoc(newDoc) { if (!_.find(this.state.docs, '_id', newDoc._id)) { this.setState({ docs: this.state.docs.concat(newDoc) }); } } removeDoc(oldDoc) { this.setState({ docs: this.state.docs.filter(doc => doc._id !== oldDoc._id) }); } /* ... */ }If you want to explore more about managing UI state, check out this article from the PouchDB website .
Ok, now we are syncing the data, but what about other assets, such as images?React Native provides a native component to display images, with some functionality that can help us.
<Image source={{uri: doc.imageUrl}} defaultSource={require('./assets/images/image-loading.png')} />The source attribute is self explanatory and the defaultSource is used to display a static image when loading the source.
This component will cache the image after the first download, so further requests of the same image won’t trigger a network request.
In the PoC application we built, we are only generating 3 different images. After that, we are using the same URLs so that we can see the application using the cached images.
var imageUrl = 'http://unsplash.it/200?random&t=' + this.imgId; localDB.put({_id: doc, content: doc, imageUrl: imageUrl}) .catch(console.log.bind(console, 'Error inserting')); ConclusionBuilding a React Native offline-first data-driven application using PouchDB and CouchDB is, in many ways, similar to the way you would build a web app using React.By using PouchDB you can access a local store that, once there is network connectivity, syncs data to and from a remote CouchDB service.