本文为转载文章, 仅用于自己的知识管理收集, 如果涉及侵权,请联系 suziwen1@gmail.com,会第一时间删除
收集该文章,并非代表本人支持文中观点,只是觉得文章内容容易引起思考,讨论,有它自有的价值转载自: https://medium.com/@duytq94/making-a-simple-note-app-with-pouchdb-in-react-native-ec4810b18a42
Introduce
Nowadays, most applications need a constant connection to the internet. Data loss or slow networks have become a problem most developers need to work around. When it comes to allowing users to go offline without losing their data, CouchDB and PouchDB offer some amazing features that are worth a look.
So the question is: can we achieve an offline-first experience using React Native? Can we store data locally? Can we sync data between multiple devices? So today I’ll try to make a note app, typical types of an application need these features to check if the required features are met.
First, determine which basic features basic note app need.
Can working with slow or no network connection.
Synchronization between devices and server when going online (with actions like creating, reading, updating, deleting notes).
Second, we’ll use CouchDB and PouchDB, what are they?
CouchDB (remote/server) is a database that completely embraces the web. Store your data with JSON documents. Access your documents with your web browser, via HTTP. Query, combine and transform your documents with JavaScript. CouchDB works well with modern web and mobile apps.
PouchDB (local/client) enables applications to store data locally while offline, then synchronize it with CouchDB and compatible servers when the application is back online, keeping the user’s data in sync no matter where they next login.
You haven’t cleared yet? Let’s come to the demo!
Demo
Implement
Setup CouchDB as a server
Access here to download a suitable package for your OS. In this article, I’m working with windows.
Follow here to install the package.
Choose Configure a Single Node
Create an account to access and control the database for both Fauxton and auth connection from the client (Fauxton is a native web-based interface built into CouchDB. It provides a basic interface to the majority of the functionality, including the ability to create, update, delete and view documents and design documents).
Setup an account with username = duytq and password = 123456 as an admin
Turn off the firewall or allow inbound port 5984, so connections from outside localhost (client devices at this case) can access to the server (if not, PouchDB at client won’t throw any error, but your result data is empty when synchronization finish).
In my case, I’m going set the firewall’s Inbound Rules
like below (Control Panel => Windows Defender Firewall => Advanced settings => Inbound Rules => New Rule…)
Result
Setup PouchDB as a client
First, init React Native project.
Then, follow here to hack PouchDB can work with React Native (I tried pouchdb-react-native but seem this library hasn’t maintained anymore and some features don’t work so I can’t use it).
Finally, install pouchdb-find and pouchdb-upsert so we can query or update data easier.
Something needs to pay attention when working with PouchDB
Limitation
- Can not store attachment since RN doesn’t support
FileReader.readAsArrayBuffer
. If you’re planning to manipulate multiple files in large size, just use PouchDB to store and sync the info file (like url, name, size…) and separate handle file (store, upload, download…) with react-native-fs. - For using SQLite, the maximum size in an item is about 2.1MB, so any field with the size bigger than that will cause the error when storing to the devices.
Attention
- Using
sync
withlive: true
thencomplete
status is never called (sequence of callback:active => change => paused
). - Using
sync
will revokepaused
twice (the docs don’t explain but I think the one is pushed and the other is pull, sincereplicate
just triggered once). - If the data has been updated recently and you just call
sync/replicate
more than 5 times, then don’t have any callback is revoked, so that why I also need to check local db to handle this case. - Calling
.cancel()
will invokecompleted
. - If you would like to sort items base on field A, need to index field A first.
- Since we use PouchDB in React Native by hacking it (disable readAsArrayBuffer) so attachment will not work. I have to encode the attachment to Base64 and use it as a field with string in this demo.
- Using
find
with a field condition don’t create an index causing a warning.
sync/replicate
with filter option won’t invoke any callback when deleting (through api or sdk’s function…) a document from the other side. Explain in the image below.- Solutions here are using update (set
_deleted = true
) instead of call delete OR add a conditiondoc._deleted === true
in your filter
- Deleting a database from remote (Fauxton) won’t cause any changes at the client, your local DB still exist.
Data structure
We only need a DB named note
Let’s talk about React Native code
Init PouchDB variables
We need 2 variables to control a local and a remote. So initialize them.
- 1export const nameIndex = {UPDATED_AT: 'index-updated_at'}
- 2
- 3const myIP = "10.68.64.131"
- 4
- 5export const remoteNoteDb = new PouchDB(`http://duytq:123456@${myIP}:5984/note`)
- 6export const localNoteDb = new PouchDB('note', {adapter: 'react-native-sqlite'})
The param’s format string is
- 1http://[username]:[password]@[ipAddress]:[port]/[dbName]
We’ll control the DB with administrative privileges. The IP is localhost’s address in your LAN network and using port as default (5984).
DB security
Designing Home screen: containing a list of notes
Home screen
The note with the latest update will be at the top of the list, meaning we need to sort item by field updated_at
and as I said, we have to index this field to make it works.
- 1remoteNoteDb.createIndex({
- 2 index: {
- 3 fields: ['updated_at'],
- 4 name: nameIndex.UPDATED_AT,
- 5 ddoc: nameIndex.UPDATED_AT,
- 6 }
- 7}).then((result) => {
- 8 console.log(TAG, result)
- 9}).catch((err) => {
- 10 console.log(TAG, err)
- 11})
Checking the database on the server, you’ll see a new document similar below
Notes
- At CouchDB, an index is also a document (a row) like others general data (all things at CouchDB is document — include index, filter condition, map-reduce…).
- You can always create this index directly from Fauxton instead of js code at the client.
- Index just need to initialize once (meaning if you initialize from js code at the client, just run the code once is enough).
When starting, we will retrieve local data (to show any information the device has stored and synchronize data from remote to update it).
- 1syncDb = () => {
- 2 this.setState({isLoading: true})
- 3 handlerSync = PouchDB.sync(remoteNoteDb, localNoteDb, {
- 4 live: true,
- 5 retry: true,
- 6 })
- 7 .on('change', (info) => {
- 8 // console.log(TAG, 'sync onChange', info)
- 9 })
- 10 .on('paused', (err) => {
- 11 // console.log(TAG, 'sync onPaused', err)
- 12 if (this.isAtCurrentScreen) {
- 13 this.getListNoteFromDb()
- 14 }
- 15 })
- 16 .on('active', () => {
- 17 // console.log(TAG, 'sync onActive')
- 18 })
- 19 .on('denied', (err) => {
- 20 // console.log(TAG, 'sync onDenied', err)
- 21 })
- 22 .on('complete', (info) => {
- 23 // console.log(TAG, 'sync onComplete', info)
- 24 })
- 25 .on('error', (err) => {
- 26 // console.log(TAG, 'sync onError', err)
- 27 })
- 28}
Synchronization run real-time with live and any data change or update has listened, so at paused
we check if the user’s not at this screen => don’t need to read the local data and render the UI.
- 1getListNoteFromDb = () => {
- 2 this.setState({isLoading: true})
- 3 localNoteDb
- 4 .find({
- 5 selector: {
- 6 updated_at: {$gt: true}
- 7 },
- 8 fields: ['_id', 'title', 'updated_at'],
- 9 use_index: nameIndex.UPDATED_AT,
- 10 sort: [{updated_at: 'desc'}]
- 11 })
- 12 .then(result => {
- 13 console.log(TAG, 'find list note', result)
- 14 this.setState({
- 15 isLoading: false,
- 16 arrNote: [...result.docs]
- 17 })
- 18 })
- 19 .catch(err => {
- 20 // console.log(TAG, 'err find list note', err)
- 21 this.setState({isLoading: false})
- 22 Toast.show(err.message)
- 23 })
- 24}
We’re checking if the user goes to detail or create new note screen and come back, we should reload the list since maybe they have changed notes.
- 1returnFromDetail = () => {
- 2 this.isAtCurrentScreen = true
- 3 this.getListNoteFromDb()
- 4}
- 5
- 6returnFromAddNewNote = () => {
- 7 this.isAtCurrentScreen = true
- 8 this.getListNoteFromDb()
- 9}
Designing Detail screen: displays detailed information, edit, delete a note
READ NOTE
We’ll use the note _id
from the previous screen (home screen) to get full note info. Using get
and pass docId as a param.
And how about a note we’re reading at detail has been deleted from another device then synchronize to the current device? How can we catch this case?
Checking and finding the current docId in an array of docs have changed at change
callback is not a good way.
So my idea is just to catch the missing error meaning current doc has been deleted then we come back to home.
- 1getDetailNoteFromDb = () => {
- 2 this.setState({isLoading: true})
- 3 localNoteDb
- 4 .get(this.idNote)
- 5 .then(result => {
- 6 // console.log(TAG, 'localNoteDb get', result)
- 7 this.setState({
- 8 isLoading: false,
- 9 detailNote: result
- 10 })
- 11 })
- 12 .catch(err => {
- 13 console.log(TAG, 'err find list note', err)
- 14 if (err.message === 'missing') {
- 15 Toast.show('This note has been deleted')
- 16 this.handleBackPress()
- 17 } else {
- 18 this.setState({isLoading: false})
- 19 Toast.show(err.message)
- 20 }
- 21 })
- 22}
Enabling synchronization at this screen too (although we have done it before at home) because it helps us instantly update the data (if not, we don’t have any trigger to know when data was updated by other devices when we’re locating at this detail screen).
- 1syncDb = () => {
- 2 handlerSync = PouchDB.sync(remoteNoteDb, localNoteDb, {
- 3 live: true,
- 4 retry: true,
- 5 })
- 6 .on('change', (info) => {
- 7 // console.log(TAG, 'sync onChange', info)
- 8 })
- 9 .on('paused', (err) => {
- 10 // console.log(TAG, 'sync onPaused', err)
- 11 this.getDetailNoteFromDb()
- 12 })
- 13 .on('active', () => {
- 14 // console.log(TAG, 'sync onActive')
- 15 })
- 16 .on('denied', (err) => {
- 17 // console.log(TAG, 'sync onDenied', err)
- 18 })
- 19 .on('complete', (info) => {
- 20 // console.log(TAG, 'sync onComplete', info)
- 21 })
- 22 .on('error', (err) => {
- 23 // console.log(TAG, 'sync onError', err)
- 24 })
- 25}
UPDATE NOTE
At this demo scope, allow user can edit title, image, content.
First, installing pouchdb-upsert for easier to update the data. If not, you have to put all item properties instead of only fields need to change.
- 1updateNote = () => {
- 2 this.setState({isLoading: true})
- 3 localNoteDb
- 4 .upsert(this.idNote, doc => {
- 5 if (this.refTextInputTitle && this.refTextInputTitle._lastNativeText) {
- 6 doc.title = this.refTextInputTitle._lastNativeText
- 7 }
- 8 if (this.state.newImage) {
- 9 doc.img = this.state.newImage
- 10 }
- 11 if (this.refTextInputContent && this.refTextInputContent._lastNativeText) {
- 12 doc.content = this.refTextInputContent._lastNativeText
- 13 }
- 14 doc.updated_at = moment().unix()
- 15 return doc
- 16 })
- 17 .then(response => {
- 18 if (response.updated) {
- 19 Toast.show('Updated')
- 20 this.setState({isLoading: false})
- 21 } else {
- 22 Toast.show('Update fail, please try again')
- 23 this.setState({isLoading: false})
- 24 }
- 25
- 26 })
- 27 .catch(err => {
- 28 console.log(TAG, err)
- 29 Toast.show(err.message)
- 30 this.setState({isLoading: false})
- 31 })
- 32}
DELETE NOTE
Deletes the document. doc
is required to be a document with at least an _id
and a _rev
property. Sending the full document will work as well.
- 1deleteNote = () => {
- 2 this.setState({isLoading: true})
- 3 localNoteDb.remove(this.idNote, this.state.detailNote._rev)
- 4 .then(response => {
- 5 if (response.ok) {
- 6 this.handleBackPress()
- 7 } else {
- 8 Toast.show('Delete note fail')
- 9 this.setState({isLoading: false})
- 10 }
- 11 })
- 12 .catch(err => {
- 13 console.log(TAG, err)
- 14 Toast.show(err.message)
- 15 this.setState({isLoading: false})
- 16 })
- 17}
After deleting successfully, go back to home screen and reload the list.
Designing Add New Note screen
Creating a new note will create an item in the DB.
Image (if exist) will be stored with format Base64 encode (react-native-image-picker supports exporting this format).
Each item can’t larger than 2.1MB as I said, so we should constraint the maximum image size is 500x500px.
- 1onSaveNotePress = () => {
- 2 Keyboard.dismiss()
- 3 if (this.refTextInputTitle && this.refTextInputTitle._lastNativeText && this.refTextInputContent && this.refTextInputContent._lastNativeText) {
- 4 this.setState({ isLoading: true })
- 5 let newNote = {
- 6 title: this.refTextInputTitle._lastNativeText,
- 7 updated_at: moment().unix(),
- 8 content: this.refTextInputContent._lastNativeText,
- 9 img: this.state.image
- 10 }
- 11 localNoteDb
- 12 .post(newNote)
- 13 .then(response => {
- 14 if (response.ok) {
- 15 Toast.show('Add new note success')
- 16 this.handleBackPress()
- 17 } else {
- 18 Toast.show('Add new note fail')
- 19 this.setState({ isLoading: false })
- 20 }
- 21 })
- 22 .catch(err => {
- 23 console.log(TAG, err)
- 24 Toast.show(err.message)
- 25 this.setState({ isLoading: false })
- 26 })
- 27 }
- 28}
After creating successfully, go back to home screen and reload the list.
Conclusion
The scope of this demo only explores the commonly used basic things of PouchDB. It has many other interesting things waiting for you to discover. Refer the docs to get more features PouchDB can help.
If you have any questions or issues with the deployment, leaving a comment and we will work together to find a solution
And of course, the source code is always available