usePouchDB

usePouchDB

  • Docs
  • API
  • Help
  • GitHub

›Basic Tutorial

Introduction

  • Quick Start
  • PouchDB and CouchDB

Basic Tutorial

  • 'Basic Tutorial: Intro'
  • Setup
  • Add the Provider
  • Add Todos
  • List all Todos
  • Update docs
  • Syncing
  • Testing
  • More

API

  • Provider
  • usePouch
  • useDoc
  • useAllDocs
  • useFind
  • useView

Update docs

Up until now we don't have any way to update our docs, i.e. mark our todos as done.

Todo component updates the doc

Updating will be the job of our updated <Todo /> component:

// Todo.js
import React from 'react'
import { usePouch } from 'use-pouchdb'

export default function Todo({ todo }) {
  const db = usePouch()

  const update = async () => {
    const doc = await db.get(todo._id)

    // check if the UI state matches the state in the database.
    if (doc.done === todo.done) {
      doc.done = !doc.done // Update the doc.

      try {
        await db.put(doc) // And put the new version into the database.
      } catch (err) {
        if (err.name === 'conflict') {
          update() // There was a conflict, try again.
        } else {
          console.error(err) // Handle other errors.
        }
      }
    }
  }

  return (
    <li className="todo-item">
      <input type="checkbox" checked={todo.done} onChange={update} />

      <span className={'todo-item__text' + (todo.done ? ' done' : '')}>
        {todo.text}
      </span>
    </li>
  )
}

Right away you might see some familiar functions, like usePouch and db.put(). But also the new db.get().

db.get() fetches a single document. All it needs is the id of the doc. Like most methods of PouchDB, it too can fail. For example, if the doc doesn't exist, or you don't have access to the database.

The hook for fetching and subscribing to a single document is useDoc.

In update we first load the document from the database. Then we check if the doc matches the UI state. And if it does, we update the done field and put the new version into the database. If the update fails with a conflict we try again.

That seems a little bit of an over engineered example. But remember PouchDB is a distributed system.

useAllDocs takes some time between receiving a change update and re-rendering a component. In this time-frame the user could have clicked. If the document did update, so that done has already the desired value, there is no need in updating the document again. It would only add in used disk space and network traffic. Which the added bonus of potential conflicts. Better check if we even need to update the doc.

db.get() and db.put() also take their time. And if in this time-frame another change did sync, we get a conflict! A conflict that throws right away: an immediate conflict.

In this example we've chosen to handle the immediate conflict by trying again. We run update again, with it still having a reverence to the old todo. It'll get the new version of the document, check if done has the desired value, and update the new doc only if not.

Ok, yes. Those two conflict examples are extremely unlikely. In this example, you would probably be save without error-handling. But in a typical app, you don't know how long something takes. What caching happens between your data source and your update component. Those examples are there to guide you to the best practice of PouchDB.

This dance of conflict resolution is what allows PouchDB to sync! It requires you to handle conflicts.

In this example we know what the desired state of a doc should be: The last user interaction. There is also almost no data lost if that assumption is wrong.

But you should generally follow the role: "Last write wins" means losing your users data!

There are two types of conflicts: immediate conflicts and eventual conflicts. You can read more about them in the PouchDB Conflicts guide.

TodoList filters the Todos

Now that we can update todos. Let's filter them!

First using the clearer, but naïve way:

The naïve way

Update <TodoList /> to be similar to this:

// TodoList.js
import React, { useState, useMemo } from 'react'
import { useAllDocs } from 'use-pouchdb'
import Todo from './Todo'
import VisibilityFilters from './VisibilityFilters'

const filters = {
  all: 'all',
  completed: 'completed',
  incomplete: 'incomplete',
}

export default function TodoList() {
  const { rows, loading } = useAllDocs({
    include_docs: true, // Load all document bodies
  })

  const [filter, setFilter] = useState(filters.all)

  const todos = useMemo(() => {
    switch (filter) {
      case filters.completed:
        return rows.filter(row => row.doc.done)

      case filters.incomplete:
        return rows.filter(row => !row.doc.done)

      case filters.all:
      default:
        return rows
    }
  }, [rows, filter])

  return (
    <>
      <ul className="todo-list">
        {(todos && todos.length) || loading
          ? todos.map(todo => <Todo key={todo.key} todo={todo.doc} />)
          : 'No todos, yay!'}
      </ul>

      <VisibilityFilters
        current={filter}
        options={filters}
        onChange={setFilter}
      />
    </>
  )
}

And the <VisibilityFilters /> component:

// VisibilityFilters.js
import React from 'react'

export default function VisibilityFilters({ current, options, onChange }) {
  return (
    <div className="visibility-filters">
      {Object.entries(options).map(([key, value]) => (
        <label key={key}>
          <input
            type="radio"
            name="visibility-filters"
            value={value}
            checked={value === current}
            onChange={() => {
              onChange(value)
            }}
          />
          <span>{value}</span>
        </label>
      ))}
    </div>
  )
}

That would be all. We now can filter todos! Show all or by completion state.

But there is one more thing! Open another tab (or better: a new window) with our Todo-App.

If you are not in private browsing all your todos should already be there! Now update them.

Disney Marvel's Loki says "Oh yes"

Your todos sync between your tabs, while the filter state is local!

The better way

But what are we doing! We fetch all documents and then manually filter them! And there is also a bug in it! When we add another document type, then they will be shown, too!

But PouchDB is a database after all! And a databases job is to index our data! Well, it can, and PouchDB has two ways to build secondary indexes. We will be using the newer Mango queries.

The hook for Mango queries is useFind (I really wanted to name it useMango but ...).

useFind is a combination of the two PouchDB methods db.find() and db.createIndex(). It can optionally check if an index exist and create it if needed.

But useFind requires a plugin: pouchdb-find.

npm
yarn
npm i -D pouchdb-find
yarn add -D pouchdb-find

Next add pouchdb-find to PouchDB in App.js:

import PouchDB from 'pouchdb-browser'
import PouchDBFind from 'pouchdb-find'
import { Provider } from 'use-pouchdb'

import AddTodo from './AddTodo'
import TodoList from './TodoList'

PouchDB.plugin(PouchDBFind) // Add pouchdb-find as a plugin

export default function App() {
  ...

After that, we can update <TodoList /> to use Mango queries:

// TodoList.js
import React, { useState } from 'react'
import { useFind } from 'use-pouchdb'
import Todo from './Todo'
import VisibilityFilters from './VisibilityFilters'

const filters = {
  all: 'all',
  completed: 'completed',
  incomplete: 'incomplete',
}

export default function TodoList() {
  const [filter, setFilter] = useState(filters.all)
  const { docs: todos, loading } = useFind(
    filter === filters.all
      ? {
          // Create and query an index for all Todos
          index: {
            fields: ['type'],
          },
          selector: {
            type: 'todo',
          },
        }
      : {
          // Create and query an index for all Todos, sorted by their done state
          index: {
            fields: ['type', 'done'],
          },
          selector: {
            type: 'todo',
            done: filter === filters.completed,
          },
        }
  )

  // todos is now an array of the documents. You must use their _id field directly!
  return (
    <>
      <ul className="todo-list">
        {(todos && todos.length) || loading
          ? todos.map(todo => <Todo key={todo._id} todo={todo} />)
          : 'No todos, yay!'}
      </ul>

      <VisibilityFilters
        current={filter}
        options={filters}
        onChange={setFilter}
      />
    </>
  )
}

We pass to useFind two sets of options.

When filter is set to all:

{
  "index": {
    "fields": ["type"]
  },
  "selector": {
    "type": "todo"
  }
}

This will create an index sorted by the type field of a document (and then their _id). And then fetch all documents that have a type field with the value of "todo".

The index object creates the index. Its fields array describes which field should be indexed. The order matters.

The selector object passes a description of the Objects to fetch. It uses the Mango Query-language. Here we request every document that has a type field with the value "todo".

The other option is for filtered todos:

{
  "index": {
    "fields": ["type", "done"]
  },
  "selector": {
    "type": "todo",
    "done": filter === filters.completed
  }
}

This will create an index where the documents are sorted first by their type field, and then their done fields, and finally their _id fields.

We then dynamically pass the value for the done field into the selector object.

Mango queries return by default max 25 docs. If you want more, then you must pass "limit": 50 to useFind:

useFind({
  index: {
    fields: ['type'],
  },
  selector: {
    type: 'todo',
  },
  limit: 50, // or more or what you need.
})

PouchDB's secondary indexes are lazy.

Most Databases will update all indexes whenever data is updated. But PouchDB will only update an index when that index is queried! It knows what did change and only update those documents.

The drawback is that if a user didn't use an index for a while, the query will take a while. But if a user never uses an index, then they never pay the update cost for that index.

Now our filter is performant and correct! And our data is still synced between tabs!

Next: we going to implement syncing between different browsers/devices.

← List all TodosSyncing →
  • Todo component updates the doc
  • TodoList filters the Todos
    • The naïve way
    • The better way
usePouchDB
Docs
Getting StartedAPI Reference
Contact
BlogGitHubStar
Impressum
Copyright © 2023 Christopher Astfalk