• 主页
  • 标签
  • 归档
  • 搜索
  • Github

March 15, 2021

Making a simple note app with PouchDB in React Native

本文为转载文章, 仅用于自己的知识管理收集, 如果涉及侵权,请联系 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

video

Screenshots

Screenshots

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 syncwithlive: true then complete status is never called (sequence of callback: active => change => paused).
  • Using sync will revoke paused twice (the docs don’t explain but I think the one is pushed and the other is pull, since replicate 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 invoke completed.
  • 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 condition doc._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.

  1. 1export const nameIndex = {UPDATED_AT: 'index-updated_at'} 

  2. 2 

  3. 3const myIP = "10.68.64.131" 

  4. 4 

  5. 5export const remoteNoteDb = new PouchDB(`http://duytq:123456@${myIP}:5984/note`) 

  6. 6export const localNoteDb = new PouchDB('note', {adapter: 'react-native-sqlite'}) 

The param’s format string is

  1. 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.

  1. 1remoteNoteDb.createIndex({ 

  2. 2 index: { 

  3. 3 fields: ['updated_at'], 

  4. 4 name: nameIndex.UPDATED_AT, 

  5. 5 ddoc: nameIndex.UPDATED_AT, 

  6. 6 } 

  7. 7}).then((result) => { 

  8. 8 console.log(TAG, result) 

  9. 9}).catch((err) => { 

  10. 10 console.log(TAG, err) 

  11. 11}) 

Checking the database on the server, you’ll see a new document similar below

Indexing updated_at field

Indexing updated_at field

Indexing updated_at field

Indexing updated_at field

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).

Synchronize data…js    28行
  1. 1syncDb = () => { 

  2. 2 this.setState({isLoading: true}) 

  3. 3 handlerSync = PouchDB.sync(remoteNoteDb, localNoteDb, { 

  4. 4 live: true, 

  5. 5 retry: true, 

  6. 6 }) 

  7. 7 .on('change', (info) => { 

  8. 8 // console.log(TAG, 'sync onChange', info) 

  9. 9 }) 

  10. 10 .on('paused', (err) => { 

  11. 11 // console.log(TAG, 'sync onPaused', err) 

  12. 12 if (this.isAtCurrentScreen) { 

  13. 13 this.getListNoteFromDb() 

  14. 14 } 

  15. 15 }) 

  16. 16 .on('active', () => { 

  17. 17 // console.log(TAG, 'sync onActive') 

  18. 18 }) 

  19. 19 .on('denied', (err) => { 

  20. 20 // console.log(TAG, 'sync onDenied', err) 

  21. 21 }) 

  22. 22 .on('complete', (info) => { 

  23. 23 // console.log(TAG, 'sync onComplete', info) 

  24. 24 }) 

  25. 25 .on('error', (err) => { 

  26. 26 // console.log(TAG, 'sync onError', err) 

  27. 27 }) 

  28. 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.

then read data from local DB to display on the screenjs    24行
  1. 1getListNoteFromDb = () => { 

  2. 2 this.setState({isLoading: true}) 

  3. 3 localNoteDb 

  4. 4 .find({ 

  5. 5 selector: { 

  6. 6 updated_at: {$gt: true} 

  7. 7 }, 

  8. 8 fields: ['_id', 'title', 'updated_at'], 

  9. 9 use_index: nameIndex.UPDATED_AT, 

  10. 10 sort: [{updated_at: 'desc'}] 

  11. 11 }) 

  12. 12 .then(result => { 

  13. 13 console.log(TAG, 'find list note', result) 

  14. 14 this.setState({ 

  15. 15 isLoading: false, 

  16. 16 arrNote: [...result.docs] 

  17. 17 }) 

  18. 18 }) 

  19. 19 .catch(err => { 

  20. 20 // console.log(TAG, 'err find list note', err) 

  21. 21 this.setState({isLoading: false}) 

  22. 22 Toast.show(err.message) 

  23. 23 }) 

  24. 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.

  1. 1returnFromDetail = () => { 

  2. 2 this.isAtCurrentScreen = true 

  3. 3 this.getListNoteFromDb() 

  4. 4} 

  5. 5 

  6. 6returnFromAddNewNote = () => { 

  7. 7 this.isAtCurrentScreen = true 

  8. 8 this.getListNoteFromDb() 

  9. 9} 

Designing Detail screen: displays detailed information, edit, delete a note

READ NOTE

Detail screen

Detail screen

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.

change callback giving us a list of docs have been changed

change callback giving us a list of docs have been changed

So my idea is just to catch the missing error meaning current doc has been deleted then we come back to home.

  1. 1getDetailNoteFromDb = () => { 

  2. 2 this.setState({isLoading: true}) 

  3. 3 localNoteDb 

  4. 4 .get(this.idNote) 

  5. 5 .then(result => { 

  6. 6 // console.log(TAG, 'localNoteDb get', result) 

  7. 7 this.setState({ 

  8. 8 isLoading: false, 

  9. 9 detailNote: result 

  10. 10 }) 

  11. 11 }) 

  12. 12 .catch(err => { 

  13. 13 console.log(TAG, 'err find list note', err) 

  14. 14 if (err.message === 'missing') { 

  15. 15 Toast.show('This note has been deleted') 

  16. 16 this.handleBackPress() 

  17. 17 } else { 

  18. 18 this.setState({isLoading: false}) 

  19. 19 Toast.show(err.message) 

  20. 20 } 

  21. 21 }) 

  22. 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).

  1. 1syncDb = () => { 

  2. 2 handlerSync = PouchDB.sync(remoteNoteDb, localNoteDb, { 

  3. 3 live: true, 

  4. 4 retry: true, 

  5. 5 }) 

  6. 6 .on('change', (info) => { 

  7. 7 // console.log(TAG, 'sync onChange', info) 

  8. 8 }) 

  9. 9 .on('paused', (err) => { 

  10. 10 // console.log(TAG, 'sync onPaused', err) 

  11. 11 this.getDetailNoteFromDb() 

  12. 12 }) 

  13. 13 .on('active', () => { 

  14. 14 // console.log(TAG, 'sync onActive') 

  15. 15 }) 

  16. 16 .on('denied', (err) => { 

  17. 17 // console.log(TAG, 'sync onDenied', err) 

  18. 18 }) 

  19. 19 .on('complete', (info) => { 

  20. 20 // console.log(TAG, 'sync onComplete', info) 

  21. 21 }) 

  22. 22 .on('error', (err) => { 

  23. 23 // console.log(TAG, 'sync onError', err) 

  24. 24 }) 

  25. 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.

Update notejs    32行
  1. 1updateNote = () => { 

  2. 2 this.setState({isLoading: true}) 

  3. 3 localNoteDb 

  4. 4 .upsert(this.idNote, doc => { 

  5. 5 if (this.refTextInputTitle && this.refTextInputTitle._lastNativeText) { 

  6. 6 doc.title = this.refTextInputTitle._lastNativeText 

  7. 7 } 

  8. 8 if (this.state.newImage) { 

  9. 9 doc.img = this.state.newImage 

  10. 10 } 

  11. 11 if (this.refTextInputContent && this.refTextInputContent._lastNativeText) { 

  12. 12 doc.content = this.refTextInputContent._lastNativeText 

  13. 13 } 

  14. 14 doc.updated_at = moment().unix() 

  15. 15 return doc 

  16. 16 }) 

  17. 17 .then(response => { 

  18. 18 if (response.updated) { 

  19. 19 Toast.show('Updated') 

  20. 20 this.setState({isLoading: false}) 

  21. 21 } else { 

  22. 22 Toast.show('Update fail, please try again') 

  23. 23 this.setState({isLoading: false}) 

  24. 24 } 

  25. 25 

  26. 26 }) 

  27. 27 .catch(err => { 

  28. 28 console.log(TAG, err) 

  29. 29 Toast.show(err.message) 

  30. 30 this.setState({isLoading: false}) 

  31. 31 }) 

  32. 32} 

DELETE NOTE

Deletes the document. doc is required to be a document with at least an _id and a _revproperty. Sending the full document will work as well.

  1. 1deleteNote = () => { 

  2. 2 this.setState({isLoading: true}) 

  3. 3 localNoteDb.remove(this.idNote, this.state.detailNote._rev) 

  4. 4 .then(response => { 

  5. 5 if (response.ok) { 

  6. 6 this.handleBackPress() 

  7. 7 } else { 

  8. 8 Toast.show('Delete note fail') 

  9. 9 this.setState({isLoading: false}) 

  10. 10 } 

  11. 11 }) 

  12. 12 .catch(err => { 

  13. 13 console.log(TAG, err) 

  14. 14 Toast.show(err.message) 

  15. 15 this.setState({isLoading: false}) 

  16. 16 }) 

  17. 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.

  1. 1onSaveNotePress = () => { 

  2. 2 Keyboard.dismiss() 

  3. 3 if (this.refTextInputTitle && this.refTextInputTitle._lastNativeText && this.refTextInputContent && this.refTextInputContent._lastNativeText) { 

  4. 4 this.setState({ isLoading: true }) 

  5. 5 let newNote = { 

  6. 6 title: this.refTextInputTitle._lastNativeText, 

  7. 7 updated_at: moment().unix(), 

  8. 8 content: this.refTextInputContent._lastNativeText, 

  9. 9 img: this.state.image 

  10. 10 } 

  11. 11 localNoteDb 

  12. 12 .post(newNote) 

  13. 13 .then(response => { 

  14. 14 if (response.ok) { 

  15. 15 Toast.show('Add new note success') 

  16. 16 this.handleBackPress() 

  17. 17 } else { 

  18. 18 Toast.show('Add new note fail') 

  19. 19 this.setState({ isLoading: false }) 

  20. 20 } 

  21. 21 }) 

  22. 22 .catch(err => { 

  23. 23 console.log(TAG, err) 

  24. 24 Toast.show(err.message) 

  25. 25 this.setState({ isLoading: false }) 

  26. 26 }) 

  27. 27 } 

  28. 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

Tagged with 文章 | 转载 | 技术
Time Flies, No Time for Nuts
Copyright © 2020 suziwen
Build with  Gatsbyjs  and  Sculpting theme