├── .babelrc.js ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── api.js ├── package.json ├── scripts ├── build.js └── update-submodules.js ├── src ├── DBContext.js ├── PouchDB.js ├── api │ ├── AllDocs.js │ ├── Find.js │ ├── Get.js │ ├── useAllDocs.js │ ├── useFind.js │ └── useGet.js ├── attachmentsAsUint8Arrays.js ├── changes.js ├── concurrent │ ├── index.js │ └── useListen.js ├── createListenHook.js ├── createPouchDB.js ├── createSubscription.js ├── debounceSetValue.js ├── getSubscription.js ├── handleError.js ├── index.js ├── renderProps.js ├── useDB.js ├── useGetSubscription.js ├── useListen.js ├── utils │ ├── createStore.js │ ├── processQueue.js │ ├── reverseArgs.js │ ├── useSubscriptionImmediateSuspense.js │ └── useSubscriptionSuspense.js └── withDB.js ├── testapp ├── .env ├── .eslintrc.json ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ └── index.html ├── src │ ├── App.js │ ├── TestSequence.js │ ├── Tests │ │ ├── AllDocuments.js │ │ ├── DontSwallowErrors.js │ │ ├── FindDocument.js │ │ ├── GetDocument.js │ │ └── index.js │ ├── index.js │ └── shared │ │ ├── DestroyDB.js │ │ ├── Test │ │ ├── Context.js │ │ ├── ErrorBoundaryAndSuspenseOrder │ │ │ ├── ErrorBoundary.js │ │ │ ├── Suspense.js │ │ │ └── index.js │ │ ├── SynchronousAndConcurrentAPIs.js │ │ ├── config.json │ │ ├── index.js │ │ └── useTestRender.js │ │ └── sleep.js └── webpack.config.js └── todoapp ├── .env ├── .eslintrc.json ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public ├── 404.html ├── index.html └── manifest.json ├── src ├── App.js ├── App.test.js ├── Container │ ├── Aside.js │ ├── index.js │ └── styles.module.css ├── Footer │ ├── ClearCompleted.js │ ├── Counter.js │ ├── Filter.js │ └── index.js ├── Input.js ├── List │ ├── Docs.js │ ├── Item.js │ └── index.js ├── ToggleAll.js ├── index.js └── serviceWorker.js └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | const { outDir = '' } = require('yargs').argv; 2 | 3 | const outDirParts = outDir.split('/'); 4 | 5 | const options = { 6 | presets: ['library-util/cjs/babel-preset', '@babel/preset-flow'], 7 | plugins: ['codegen'] 8 | }; 9 | 10 | const target = ['browser', 'node'].find(target => outDirParts.includes(target)); 11 | 12 | if (target) { 13 | options.plugins.push([ 14 | 'transform-rename-import', 15 | { original: 'pouchdb', replacement: `pouchdb-${target}` } 16 | ]); 17 | } 18 | 19 | module.exports = options; 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = false 7 | 8 | [*.js] 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_size = 2 12 | 13 | [*.json] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.yml] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./node_modules/@postinumero/eslintrc/index.json"], 3 | "rules": { 4 | "jsx-a11y/anchor-has-content": "off", 5 | "jsx-a11y/label-has-associated-control": "off", 6 | "jsx-a11y/label-has-for": "off", 7 | "jsx-a11y/no-autofocus": "off", 8 | "operator-assignment": ["error", "never"], 9 | "no-param-reassign": "off", 10 | "camelcase": [ 11 | "error", 12 | { 13 | "allow": ["_local_seq"], 14 | "properties": "never" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /browser/ 2 | /build/ 3 | /cjs/ 4 | /concurrent/ 5 | /coverage/ 6 | /es/ 7 | /node/ 8 | /node_modules/ 9 | /test/ 10 | /test-*/ 11 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@postinumero/prettierrc'); 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.1.0 2 | 3 | - Add `useAllDocs` and `` [#23](https://github.com/ArnoSaine/react-pouchdb/pull/23) ([@Terreii](https://github.com/Terreii)) 4 | 5 | # 2.0.1 6 | 7 | - Inline a dependency that uses ES module syntax (Fix [#13](https://github.com/ArnoSaine/react-pouchdb/issues/13)) 8 | 9 | # 2.0.0 10 | 11 | - (No changes) 12 | 13 | # 2.0.0-beta.3 14 | 15 | - Use use-subscription 16 | 17 | # 2.0.0-beta.2 18 | 19 | - Fix excessive re-rendering from `bulkDocs` 20 | - Fix keeping original docs array always internal 21 | - Update dependencies 22 | 23 | # 2.0.0-beta.1 24 | 25 | - Optimize caching change requests 26 | - Fix memory leak in listening changes 27 | - Fix [#7](https://github.com/ArnoSaine/react-pouchdb/issues/7): Show errors from PouchDB 28 | 29 | # 2.0.0-beta.0 30 | 31 | - Add concurrent API variant 32 | - **Breaking change:** In case of missing document `useGet` returns `null` (same for `doc` value in ``) 33 | - **Breaking change:** In case of removed document `useGet` returns `{"_id": ..., "_rev": ..., "_deleted": true}` (same for `doc` value in ``) 34 | - **Breaking change:** Remove `render` and `component` render props from `` and `` → Use `children` render prop 35 | - **Breaking change:** `children` render prop behaves like `render` and `component` render prop in v1 36 | - Update dependencies 37 | 38 | # 1.0.0 39 | 40 | - Add Hooks API 41 | - Add optional `db` prop to `` and `` 42 | - **Breaking change:** remove `exists` prop from `` render methods. Use `!!doc` 43 | - **Breaking change:** remove `attachment` prop from `` render methods. Use `doc._attachments` 44 | - **Breaking change:** `component` and `render` render props require the use of `` as a parent component 45 | - **Breaking change:** `` render props are called even if document does not exist 46 | - Update DB connection when `` props change 47 | - Fix infinite suspense loop if database request throws error 48 | - Update dependencies 49 | 50 | # 0.3.2 51 | 52 | - Fix [#5](https://github.com/ArnoSaine/react-pouchdb/issues/5): Using `` with `sort` option and remote database 53 | 54 | # 0.3.1 55 | 56 | - Update package-lock.json 57 | 58 | # 0.3.0 59 | 60 | - Update dependencies (major) 61 | 62 | # 0.2.1 63 | 64 | - Update dependencies (minor) 65 | 66 | # 0.2.0 67 | 68 | - Use new React 16.3 context and ref APIs. Ready for React 17. 69 | 70 | # 0.1.6 71 | 72 | - Update `peerDependencies`: Remove compatibility with React 17 73 | 74 | # 0.1.5 75 | 76 | - [#3](https://github.com/ArnoSaine/react-pouchdb/pull/3) Immutable manipulation on docs array in find. ([@bkniffler](https://github.com/bkniffler)) 77 | 78 | # 0.1.4 79 | 80 | - Fix closing the database on unmount 81 | 82 | # 0.1.3 83 | 84 | - Pool PouchDB connections 85 | 86 | # 0.1.2 87 | 88 | - Add package.json repository field 89 | - Add missing dependency @babel/runtime 90 | - Update example 91 | 92 | # 0.1.1 93 | 94 | - Fix gh-pages example 95 | - Update readme 96 | 97 | # 0.1.0 98 | 99 | - First release 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-present, Arno Saine 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-pouchdb 2 | 3 | React wrapper for PouchDB that also subscribes to changes. 4 | 5 | [TodoMVC example](https://arnosaine.github.io/react-pouchdb/) 6 | 7 | ## Contents 8 | 9 | - [Examples](#examples) 10 | - [Hooks](#hooks) 11 | - [Components](#components) 12 | - [API](#api) 13 | - [`useGet`](#usegetdb-options) 14 | - [`useFind`](#usefinddb-options) 15 | - [`useAllDocs`](#usealldocsdb-options) 16 | - [`useDB`](#usedbdb) 17 | - [``](#pouchdb) 18 | - [``](#get) 19 | - [``](#find) 20 | - [``](#alldocs) 21 | - [`withDB`](#withdbdb-component) 22 | - [API Variants](#api-variants) 23 | - [Synchronous](#synchronous) 24 | - [Concurrent](#concurrent) 25 | - [Package dependencies](#package-dependencies) 26 | 27 | ## Examples 28 | 29 | ### Hooks 30 | 31 | ```js 32 | import { Suspense } from "react"; 33 | import { PouchDB, useFind, useDB } from "react-pouchdb"; 34 | 35 | function MyComponent() { 36 | const docs = useFind({ 37 | selector: { 38 | name: { $gte: null } 39 | }, 40 | sort: ["name"] 41 | }); 42 | const db = useDB(); 43 | 44 | return ( 45 |
    46 | {docs.map(doc => ( 47 |
  • 48 | {doc.name} 49 | 50 |
  • 51 | ))} 52 |
53 | ); 54 | } 55 | 56 | 57 | 58 | 59 | 60 | ; 61 | ``` 62 | 63 | ### Components 64 | 65 | ```js 66 | import { Suspense } from "react"; 67 | import { PouchDB, Find } from "react-pouchdb"; 68 | 69 | 70 | 71 | ( 77 |
    78 | {docs.map(doc => ( 79 |
  • 80 | {doc.name} 81 | 82 |
  • 83 | ))} 84 |
85 | )} 86 | /> 87 |
88 |
; 89 | ``` 90 | 91 | ## API 92 | 93 | ### `useGet([db,] options)` 94 | 95 | Get document and listen to changes. 96 | 97 | **`db: string|object` (optional)** 98 | 99 | Override the context value or use as an alternative to ``. 100 | 101 | **`options: object`** 102 | 103 | Options to [`get`](https://pouchdb.com/api.html#fetch_document). If **other** than `id`, `attachments`, `ajax` or `binary` options are set, live changes are disabled. 104 | 105 | **`options.attachments: bool|string` (optional)** 106 | 107 | Include document attachments. Set to `"u8a"` to get attachments as `Uint8Array`s. 108 | 109 | **Returns** 110 | 111 | | Value | Description | Example | 112 | | --- | --- | --- | 113 | | undefined | Request is pending (only in [Concurrent API](#concurrent)) | `undefined` | 114 | | null | Missing document | `null` | 115 | | Document | Found document | `{"_id": ..., "_rev": ..., ...}` | 116 | | Deleted document | Deleted document | `{"_id": ..., "_rev": ..., "_deleted": true}` | 117 | 118 | ```js 119 | import { useGet } from "react-pouchdb"; 120 | 121 | function MyComponent() { 122 | const doc = useGet({ id: "mydoc" }); 123 | return
{doc.name}
; 124 | } 125 | ``` 126 | 127 | ### `useFind([db,] options)` 128 | 129 | Find documents and listen to changes. 130 | 131 | **`db: string|object` (optional)** 132 | 133 | Override the context value or use as an alternative to ``. 134 | 135 | **`options: object`** 136 | 137 | Options to [`find`](https://pouchdb.com/api.html#query_index). 138 | 139 | **`options.sort: (string|object)[]` (optional)** 140 | 141 | If **sort** is present, then it will be used to create a mango index with [`createIndex`](https://pouchdb.com/api.html#create_index). 142 | 143 | **Returns** 144 | 145 | | Value | Description | Example | 146 | | --- | --- | --- | 147 | | undefined | Request is pending (only in [Concurrent API](#concurrent)) | `undefined` | 148 | | Array | List of documents | `[{"_id": ..., "_rev": ..., ...}, ...]` | 149 | 150 | ```js 151 | import { useFind } from "react-pouchdb"; 152 | 153 | function MyComponent() { 154 | const docs = useFind({ 155 | selector: { 156 | name: { $gte: null } 157 | }, 158 | sort: ["name"] 159 | }); 160 | return ( 161 |
    162 | {docs.map(doc => ( 163 |
  • {doc.name}
  • 164 | ))} 165 |
166 | ); 167 | } 168 | ``` 169 | 170 | ### `useAllDocs([db,] options)` 171 | 172 | Get multiple rows of document meta-data (**id** and **rev**) with optional the documents and listen to changes. 173 | 174 | **`db: string|object` (optional)** 175 | 176 | Override the context value or use as an alternative to ``. 177 | 178 | **`options: object`** 179 | 180 | Options to [`allDocs`](https://pouchdb.com/api.html#batch_fetch). 181 | 182 | **`options.attachments: bool|string` (optional)** 183 | 184 | Include document attachments. Set to `"u8a"` to get attachments as `Uint8Array`s. 185 | 186 | **Returns** 187 | 188 | | Value | Description | Example | 189 | | --- | --- | --- | 190 | | undefined | Request is pending (only in [Concurrent API](#concurrent)) | `undefined` | 191 | | Array | List of document meta data with the document. The rows-field from `allDocs`. | `[{"id": "doc_id", "key": "doc_id", "value": { "rev": ... }, "doc": { ... } }, ...]` | 192 | 193 | ```js 194 | import { useAllDocs } from "react-pouchdb"; 195 | 196 | function MyComponent() { 197 | const rows = useAllDocs({ 198 | include_docs: true, 199 | startkey: "profile_", 200 | endkey: "profile_\uffff" 201 | }); 202 | return ( 203 |
    204 | {rows.map(row => ( 205 |
  • {row.doc.name}
  • 206 | ))} 207 |
208 | ); 209 | } 210 | ``` 211 | 212 | ### `useDB([db])` 213 | 214 | Get the PouchDB instance from the context. 215 | 216 | **`db: string|object` (optional)** 217 | 218 | Override the context value or use as an alternative to ``. 219 | 220 | ```js 221 | import { useDB } from "react-pouchdb"; 222 | 223 | function MyComponent({ title }) { 224 | const db = useDB(); 225 | return ; 226 | } 227 | ``` 228 | 229 | ### `` 230 | 231 | Connect to a database and provide it from the context to other components and hooks. 232 | 233 | **`name: string`** 234 | 235 | **`maxListeners: number`** 236 | 237 | Similar change requests are detected and cached. You might still need to increase the number of `maxListeners`, if you use `useGet` / `` with lots of different options. 238 | 239 | **`...rest: any`** 240 | 241 | Other props are passed to [PouchDB constructor](https://pouchdb.com/api.html#create_database) as a second argument. 242 | 243 | ```js 244 | 245 | 246 | 247 | ``` 248 | 249 | ### `` 250 | 251 | Get document and listen to changes. 252 | 253 | **`db: string|object` (optional)** 254 | 255 | Override the context value or use as an alternative to ``. 256 | 257 | ```js 258 | 259 | ``` 260 | 261 | **`id: string`** 262 | 263 | `docId`. 264 | 265 | **`children: func|component|element`** 266 | 267 | Function is called / component is rendered / element is cloned with props `db` and `doc`. See [`useGet`](#usegetdb-options) return value for possible values for `doc`. 268 | 269 | ```js 270 |

{doc.title}

} /> 271 | ``` 272 | 273 | **`attachments: bool|string`** 274 | 275 | Include document attachments. Set to `"u8a"` to get attachments as `Uint8Array`s. 276 | 277 | ```js 278 | ( 282 | <> 283 |

{doc.title}

284 | {doc._attachments["att.txt"].data} 285 | 286 | )} 287 | /> 288 | ``` 289 | 290 | **`...rest: any`** 291 | 292 | Other props are passed to [`get`](https://pouchdb.com/api.html#fetch_document) method as second argument. If **other** than `attachments`, `ajax` or `binary` props are provided, live changes are disabled. 293 | 294 | ### `` 295 | 296 | Find documents and listen to changes. 297 | 298 | **`db: string|object` (optional)** 299 | 300 | Override the context value or use as an alternative to ``. 301 | 302 | ```js 303 | 304 | ``` 305 | 306 | **`selector: object`** 307 | 308 | **`sort: array`** 309 | 310 | If **sort** is present, then it will be used to create a mango index with [`createIndex`](https://pouchdb.com/api.html#create_index). 311 | 312 | **`limit: number`** 313 | 314 | **`skip: number`** 315 | 316 | See [`find`](https://pouchdb.com/api.html#query_index). 317 | 318 | **`children: func|component|element`** 319 | 320 | Function is called / component is rendered / element is cloned with props `db` and `docs`. See [`useFind`](#usefinddb-options) return value for possible values for `docs`. 321 | 322 | ```js 323 | ( 329 |
    330 | {docs.map(doc => ( 331 |
  • {doc.name}
  • 332 | ))} 333 |
334 | )} 335 | /> 336 | ``` 337 | 338 | ### `` 339 | 340 | Get multiple rows of document meta-data (**id** and **rev**) with optional the documents and listen to changes. 341 | 342 | **`db: string|object` (optional)** 343 | 344 | Override the context value or use as an alternative to ``. 345 | 346 | ```js 347 | 348 | ``` 349 | 350 | **`include_docs: bool`** 351 | 352 | **`conflicts: bool`** 353 | 354 | **`attachments: bool|string`** 355 | 356 | Include document attachments. Set to `"u8a"` to get attachments as `Uint8Array`s. 357 | 358 | **`startkey: string`** 359 | 360 | **`endkey: string`** 361 | 362 | **`descending: bool`** 363 | 364 | **`keys: string[]`** 365 | 366 | **`limit: number`** 367 | 368 | **`skip: number`** 369 | 370 | See [`allDocs`](https://pouchdb.com/api.html#batch_fetch). 371 | 372 | **`children: func|component|element`** 373 | 374 | Function is called / component is rendered / element is cloned with props `db` and `rows`. See [`useAllDocs`](#usealldocsdb-options) return value for possible values for `rows`. 375 | 376 | ```js 377 | ( 382 |
    383 | {rows.map(row => ( 384 |
  • {row.doc.name}
  • 385 | ))} 386 |
387 | )} 388 | /> 389 | ``` 390 | 391 | ### `withDB([db,] Component)` 392 | 393 | Higher-order component for accessing the PouchDB instance anywhere in the `` children. Note that for convenience `` and `` render methods will be passed the `db` prop as well. 394 | 395 | ```js 396 | import { withDB } from "react-pouchdb"; 397 | 398 | const MyComponent = withDB(({ db, title }) => ( 399 | 400 | )); 401 | ``` 402 | 403 | ## API Variants 404 | 405 | ### Synchronous 406 | 407 | It is guaranteed that the API returns with a final response value from PouchDB. Because of this, requests are made sequentially. 408 | 409 | Import from `react-pouchdb` to use the Synchronous API. Example: 410 | 411 | ```js 412 | import { useFind, useDB } from "react-pouchdb"; 413 | ``` 414 | 415 | ### Concurrent 416 | 417 | Requests are made simultaneously. The drawback is that while a request is pending, the API returns `undefined`, which user must handle without error, i.e. render `null` and use `` to show a loading indicator. 418 | 419 | Import from `react-pouchdb/concurrent` to use the Concurrent API. Example: 420 | 421 | ```js 422 | import { useFind, useDB } from "react-pouchdb/concurrent"; 423 | ``` 424 | 425 | ## Package dependencies 426 | 427 | The package expects `pouchdb` to be available. If you use `pouchdb-browser` or `pouchdb-node`, import from `react-pouchdb/browser` or `react-pouchdb/node` respectively. 428 | 429 | If you use `pouchdb-browser` or `pouchdb-node` and [Concurrent API](#concurrent), import from `react-pouchdb/browser/concurrent` or `react-pouchdb/node/concurrent`. 430 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | const { dirname, relative } = require('path'); 2 | 3 | module.exports = module => { 4 | const root = 5 | relative(dirname(module.filename), `${__dirname}/src`).replace( 6 | /\\/g, 7 | '/' 8 | ) || '.'; 9 | module.exports = `import apiAllDocs from '${root}/api/AllDocs'; 10 | import apiFind from '${root}/api/Find'; 11 | import apiGet from '${root}/api/Get'; 12 | import apiUseFind from '${root}/api/useFind'; 13 | import apiUseGet from '${root}/api/useGet'; 14 | import apiUseAllDocs from '${root}/api/useAllDocs'; 15 | import useListen from './useListen'; 16 | 17 | export PouchDB from '${root}/PouchDB'; 18 | export withDB from '${root}/withDB'; 19 | export * as DBContext from '${root}/DBContext'; 20 | export useDB from '${root}/useDB'; 21 | export const useFind = apiUseFind(useListen); 22 | export const useGet = apiUseGet(useListen); 23 | export const useAllDocs = apiUseAllDocs(useListen); 24 | export const AllDocs = apiAllDocs(useAllDocs); 25 | export const Find = apiFind(useFind); 26 | export const Get = apiGet(useGet);`; 27 | }; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pouchdb", 3 | "version": "2.1.0", 4 | "description": "React wrapper for PouchDB that also subscribes to changes.", 5 | "main": "cjs", 6 | "module": "es", 7 | "scripts": { 8 | "build": "npm run inbuild --", 9 | "build:todoapp": "npx babel src --out-dir todoapp/node_modules/react-pouchdb/browser/es", 10 | "deploy": "cd todoapp && npm run build && cd .. && gh-pages --dist todoapp/build", 11 | "dev:testapp": "concurrently \"npm run watch\" \"npm link && cd testapp && npm link react-pouchdb && npm start\"", 12 | "dev:todoapp": "concurrently \"npm run watch\" \"npm link && cd todoapp && npm link react-pouchdb && npm start\"", 13 | "eslint": "eslint {{.,testapp,todoapp},{scripts,src,testapp/src,todoapp/src}/**}/*.js --fix", 14 | "inbuild": "node -r esm scripts/build --source-maps", 15 | "lint": "npm run eslint && npm run prettier", 16 | "postbuild": "npm run update-submodules", 17 | "postpublish": "cd testapp && npm install react-pouchdb@latest && cd ../todoapp && npm install react-pouchdb@latest", 18 | "prebuild": "rimraf browser cjs es node", 19 | "prepack": "npm run build", 20 | "prettier": "npm run prettier:write -- {{.,testapp,todoapp},{scripts,src,testapp/src,todoapp/src}/**}/*.{js,json} !./package-lock.json", 21 | "prettier:write": "prettier --config .prettierrc.js --write", 22 | "preversion": "npm run lint", 23 | "update-submodules": "node -r esm scripts/update-submodules", 24 | "version": "npm run update-submodules --", 25 | "watch": "npm run build && concurrently \"npm run inbuild -- --watch --skip-initial-build\" \"npm run update-submodules\"" 26 | }, 27 | "keywords": [ 28 | "react", 29 | "pouchdb", 30 | "couchdb" 31 | ], 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/ArnoSaine/react-pouchdb.git" 35 | }, 36 | "license": "ISC", 37 | "author": { 38 | "name": "Arno Saine", 39 | "email": "arno@mowhi.com" 40 | }, 41 | "files": [ 42 | "browser", 43 | "cjs", 44 | "es", 45 | "node", 46 | "concurrent" 47 | ], 48 | "peerDependencies": { 49 | "react": "^16.8 || 17" 50 | }, 51 | "dependencies": { 52 | "@babel/runtime": "^7.6.3", 53 | "fast-json-stable-stringify": "^2.0.0", 54 | "hoist-non-react-statics": "^3.3.0", 55 | "lodash": "^4.17.15", 56 | "pouchdb-collate": "^7.1.1", 57 | "pouchdb-find": "^7.1.1", 58 | "pouchdb-selector-core": "^7.1.1", 59 | "use-subscription": "^1.1.1" 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "^7.6.4", 63 | "@babel/preset-flow": "^7.0.0", 64 | "babel-plugin-codegen": "^3.0.0", 65 | "babel-plugin-transform-rename-import": "^2.3.0", 66 | "concurrently": "^5.0.0", 67 | "cross-spawn": "^7.0.1", 68 | "esm": "^3.2.25", 69 | "fs-extra": "^8.1.0", 70 | "gh-pages": "^2.1.1", 71 | "library-util": "^0.5.0", 72 | "pouchdb": "^7.1.1", 73 | "pouchdb-browser": "^7.1.1", 74 | "react": "^16.10.2", 75 | "react-dom": "^16.10.2", 76 | "yargs": "^14.2.0" 77 | }, 78 | "lint-staged": { 79 | "*.js": [ 80 | "eslint --fix", 81 | "npm run prettier:write", 82 | "git add" 83 | ], 84 | "*.{css,json,md}": [ 85 | "npm run prettier:write", 86 | "git add" 87 | ] 88 | }, 89 | "husky": { 90 | "hooks": { 91 | "pre-commit": "lint-staged" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | import spawn from 'cross-spawn'; 2 | import flatten from 'lodash/flatten'; 3 | import fromPairs from 'lodash/fromPairs'; 4 | import kebabCase from 'lodash/kebabCase'; 5 | import yargs from 'yargs'; 6 | 7 | const { 8 | argv, 9 | argv: { outDir } 10 | } = yargs(process.argv.slice(2)); 11 | 12 | const args = flatten( 13 | Object.entries( 14 | fromPairs( 15 | Object.entries(argv) 16 | .filter(([name]) => name !== '_' && name[0] !== '$') 17 | .map(([name, value]) => [ 18 | kebabCase(name), 19 | value === true ? undefined : value 20 | // typeof value === 'boolean' ? (value ? undefined : 'false') : value 21 | ]) 22 | ) 23 | ) 24 | .filter(([name]) => name !== 'out-dir') 25 | .map(([key, value]) => [`--${key}`, value]) 26 | ).filter(Boolean); 27 | 28 | flatten( 29 | [[], ['browser'], ['node']].map(path => 30 | ['es', 'cjs'].map(target => 31 | [...(outDir ? [outDir] : []), ...path, target].join('/') 32 | ) 33 | ) 34 | ).forEach(outDir => 35 | spawn('npx', [...`babel src --out-dir ${outDir}`.split(' '), ...args], { 36 | stdio: 'inherit' 37 | }) 38 | ); 39 | -------------------------------------------------------------------------------- /scripts/update-submodules.js: -------------------------------------------------------------------------------- 1 | import { ensureDirSync, readJsonSync, writeJsonSync } from 'fs-extra'; 2 | 3 | const updateJsonSync = (file, updater) => 4 | writeJsonSync(file, updater(readJsonSync(file)), { spaces: 2 }); 5 | 6 | ['browser', 'node'].forEach(target => { 7 | writeJsonSync( 8 | `${target}/package.json`, 9 | { 10 | name: `@${process.env.npm_package_name}/${target}`, 11 | version: process.env.npm_package_version, 12 | main: 'cjs', 13 | module: 'es' 14 | }, 15 | { spaces: 2 } 16 | ); 17 | }); 18 | 19 | ['browser/concurrent', 'node/concurrent', 'concurrent'].forEach(target => { 20 | ensureDirSync(target); 21 | writeJsonSync( 22 | `${target}/package.json`, 23 | { 24 | name: `@${process.env.npm_package_name}/${target}`, 25 | version: process.env.npm_package_version, 26 | main: '../cjs/concurrent', 27 | module: '../es/concurrent' 28 | }, 29 | { spaces: 2 } 30 | ); 31 | }); 32 | 33 | [ 34 | 'testapp/package.json', 35 | 'testapp/package-lock.json', 36 | 'todoapp/package.json', 37 | 'todoapp/package-lock.json' 38 | ].forEach(file => 39 | updateJsonSync(file, value => ({ 40 | ...value, 41 | version: process.env.npm_package_version 42 | })) 43 | ); 44 | -------------------------------------------------------------------------------- /src/DBContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const context = createContext(); 4 | export default context; 5 | export const { Consumer, Provider } = context; 6 | -------------------------------------------------------------------------------- /src/PouchDB.js: -------------------------------------------------------------------------------- 1 | import { Provider } from './DBContext'; 2 | import { useDBOptions } from './useDB'; 3 | 4 | export default function PouchDB({ children, ...options }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /src/api/AllDocs.js: -------------------------------------------------------------------------------- 1 | import renderProps from '../renderProps'; 2 | 3 | export default renderProps(rows => ({ rows }), { 4 | callee: '', 5 | example: '' 6 | }); 7 | -------------------------------------------------------------------------------- /src/api/Find.js: -------------------------------------------------------------------------------- 1 | import renderProps from '../renderProps'; 2 | 3 | export default renderProps(docs => ({ docs }), { 4 | callee: '', 5 | example: '' 6 | }); 7 | -------------------------------------------------------------------------------- /src/api/Get.js: -------------------------------------------------------------------------------- 1 | import renderProps from '../renderProps'; 2 | 3 | export default renderProps(doc => ({ doc }), { 4 | callee: '', 5 | example: '' 6 | }); 7 | -------------------------------------------------------------------------------- /src/api/useAllDocs.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import stringify from 'fast-json-stable-stringify'; 3 | import reverseArgs from '../utils/reverseArgs'; 4 | import attachmentsAsUint8Arrays from '../attachmentsAsUint8Arrays'; 5 | import changes from '../changes'; 6 | import useDB from '../useDB'; 7 | 8 | const UINT8ARRAY = 'u8a'; 9 | 10 | /** 11 | * Converts all attachments of a document into Uint8Arrays, if the binary option is set. 12 | * @param {boolean} binary Should the attachments be converted into an Uint8Array. 13 | * @param {object} doc The document. 14 | */ 15 | async function formatDocAttachments(binary, doc) { 16 | return binary && doc && !doc._deleted && '_attachments' in doc 17 | ? { 18 | ...doc, 19 | _attachments: await attachmentsAsUint8Arrays(doc._attachments) 20 | } 21 | : doc; 22 | } 23 | 24 | /** 25 | * Transforms a change object into a row object. 26 | * @param {boolean} includeDocs Should documents be included in the allDocs result. 27 | * @param {boolean} binary Should the attachments be converted into an Uint8Array. 28 | * @param {object} change PouchDB-Change-Object. 29 | */ 30 | async function transformToRow(includeDocs, binary, change) { 31 | return { 32 | id: change.id, 33 | key: change.id, 34 | value: change.changes[0], 35 | doc: 36 | includeDocs && change.doc 37 | ? await formatDocAttachments(binary, change.doc) 38 | : undefined 39 | }; 40 | } 41 | 42 | /** 43 | * Converts all attachments into Uint8Arrays. 44 | * The rows will be updated in place, while the documents are copied. 45 | * @param {Object[]} rows Rows that should be updated IN PLACE. 46 | */ 47 | function formatRowsAttachments(binary, rows) { 48 | // The mapped value is the Promise. The rows will be updated in place! 49 | const converting = rows.map(async row => { 50 | if (row.doc && row.doc._attachments) { 51 | row.doc = await formatDocAttachments(binary, row.doc); 52 | } 53 | }); 54 | 55 | return Promise.all(converting); 56 | } 57 | 58 | export default useListen => 59 | reverseArgs(function useAllDocs(options = {}, db) { 60 | db = useDB(db, { 61 | callee: 'useAllDocs', 62 | example: 'useAllDocs(db, options)' 63 | }); 64 | 65 | const { 66 | startkey, 67 | endkey, 68 | descending, 69 | attachments, 70 | limit, 71 | skip, 72 | ...otherOptions 73 | } = options; 74 | const binary = attachments === UINT8ARRAY; 75 | 76 | const optionsWithAttachmentAndBinaryOption = useMemo( 77 | () => ({ 78 | // binary option will be overwritten by otherOptions.binary 79 | // so that a Blob can be used. 80 | binary, 81 | startkey, 82 | endkey, 83 | descending, 84 | limit, 85 | skip, 86 | ...otherOptions, 87 | attachments: !!attachments 88 | }), 89 | [ 90 | binary, 91 | startkey, 92 | endkey, 93 | descending, 94 | limit, 95 | skip, 96 | stringify(otherOptions), 97 | !!attachments 98 | ] 99 | ); 100 | const keysMode = options.keys != null; 101 | const keyMode = options.key != null; 102 | 103 | const changesOptions = useMemo(() => { 104 | const changesOptions = { 105 | live: true, 106 | include_docs: optionsWithAttachmentAndBinaryOption.include_docs, 107 | since: 'now', 108 | // Documents are kept in memory. 'complete' event can return an empty array. 109 | return_docs: false 110 | }; 111 | // add keys/key to only subscribe to those. 112 | if (keyMode) { 113 | changesOptions.doc_ids = [optionsWithAttachmentAndBinaryOption.key]; 114 | } else if (keysMode) { 115 | changesOptions.doc_ids = optionsWithAttachmentAndBinaryOption.keys; 116 | } 117 | return changesOptions; 118 | }, [optionsWithAttachmentAndBinaryOption, keysMode, keyMode]); 119 | 120 | return useListen(db, options, async setValue => { 121 | const { rows } = await db.allDocs(optionsWithAttachmentAndBinaryOption); 122 | if (binary) { 123 | await formatRowsAttachments(binary, rows); 124 | } 125 | const update = () => setValue([...rows]); 126 | update(); 127 | const toRow = transformToRow.bind(null, options.include_docs, binary); 128 | 129 | return db::changes(changesOptions, async change => { 130 | const { id, deleted } = change; 131 | 132 | // guards that check if the document's id is between startkey and endkey. 133 | // But only if they exist. 134 | if ( 135 | startkey != null && 136 | ((descending && id > startkey) || (!descending && id < startkey)) 137 | ) { 138 | return; 139 | } 140 | if ( 141 | endkey != null && 142 | ((descending && id < endkey) || 143 | (!descending && id > endkey) || 144 | // if inclusive_end is omitted, it defaults to true 145 | (options.inclusive_end === false && endkey === id)) 146 | ) { 147 | return; 148 | } 149 | 150 | const index = rows.findIndex(row => row.id === id); 151 | const found = index !== -1; 152 | 153 | // Document was deleted 154 | if (deleted) { 155 | if (found) { 156 | // remove row 157 | rows.splice(index, 1); 158 | 159 | // ranges mode (with startkey and endkey) 160 | // If it was on the limit load the next row/document. 161 | if (rows.length + 1 === limit && !keysMode && !keyMode) { 162 | const lastId = rows[rows.length - 1].id; 163 | const replacementRows = await db.allDocs({ 164 | ...optionsWithAttachmentAndBinaryOption, 165 | startkey: lastId, 166 | skip: 1, 167 | limit: 1 168 | }); 169 | if (binary) { 170 | await formatRowsAttachments(binary, replacementRows.rows); 171 | } 172 | rows.push(...replacementRows.rows); 173 | } else if (keysMode && !keyMode) { 174 | const replacementRows = await db.allDocs({ 175 | ...optionsWithAttachmentAndBinaryOption, 176 | keys: [id] 177 | }); 178 | if (binary) { 179 | await formatRowsAttachments(binary, replacementRows.rows); 180 | } 181 | rows.splice(index, 0, ...replacementRows.rows); 182 | } 183 | 184 | update(); 185 | } 186 | } else if (found) { 187 | rows[index] = await toRow(change); 188 | update(); 189 | } else { 190 | // add the new row 191 | let insertIndex = rows.findIndex( 192 | descending 193 | ? row => row.id < id // if descending find the first row with a smaller id 194 | : row => row.id > id // else find the first row with a bigger id 195 | ); 196 | if (insertIndex === -1) { 197 | // If no index was found, then push it to the end 198 | if (limit != null && rows.length === limit) { 199 | // But not if the limit is already exceeded 200 | return; 201 | } 202 | insertIndex = rows.length; 203 | } 204 | 205 | rows.splice(insertIndex, 0, await toRow(change)); 206 | 207 | if (limit != null && rows.length > limit) { 208 | // remove the last row if the limit was exceeded. 209 | rows.pop(); 210 | } 211 | update(); 212 | } 213 | }); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /src/api/useFind.js: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb'; 2 | import find from 'pouchdb-find'; 3 | import { collate } from 'pouchdb-collate'; 4 | import { matchesSelector } from 'pouchdb-selector-core'; 5 | import reverseArgs from '../utils/reverseArgs'; 6 | import changes from '../changes'; 7 | import useDB from '../useDB'; 8 | 9 | PouchDB.plugin(find); 10 | 11 | const changesOptions = { 12 | live: true, 13 | include_docs: true, 14 | since: 'now', 15 | // Documents are kept in memory. 'complete' event can return an empty array. 16 | return_docs: false 17 | }; 18 | 19 | export default useListen => 20 | reverseArgs(function useFind(options, db) { 21 | db = useDB(db, { 22 | callee: 'useFind', 23 | example: 'useFind(db, options)' 24 | }); 25 | const { selector, limit, skip, sort } = options; 26 | 27 | return useListen(db, options, async setValue => { 28 | if (sort) { 29 | await db.createIndex({ 30 | index: { 31 | fields: sort.map(field => 32 | typeof field === 'object' ? Object.keys(field)[0] : field 33 | ) 34 | } 35 | }); 36 | } 37 | let { docs } = await db.find(options); 38 | const update = () => setValue([...docs]); 39 | update(); 40 | // To find deleted and other non-matching documents, listen all changes and use selector in 'change' event. 41 | return db::changes( 42 | changesOptions, // 43 | async ({ deleted, doc }) => { 44 | const index = docs?.findIndex(({ _id }) => doc._id === _id); 45 | const found = index !== -1; 46 | // Document was deleted or it does not match the selector? 47 | if (deleted || (selector && !matchesSelector(doc, selector))) { 48 | if (found) { 49 | // Remove. 50 | docs.splice(index, 1); 51 | const { length } = docs; 52 | // At the limit? 53 | if (length + 1 === limit) { 54 | const { 55 | docs: [replacementDoc] 56 | } = await db.find({ 57 | ...options, 58 | limit: 1, 59 | skip: (options.skip || 0) + length 60 | }); 61 | if (replacementDoc) { 62 | docs.push(replacementDoc); 63 | } 64 | } 65 | update(); 66 | } 67 | } else { 68 | if (found) { 69 | // Update. 70 | docs[index] = doc; 71 | } else { 72 | // Create. 73 | if (!docs) { 74 | docs = []; 75 | } 76 | docs.push(doc); 77 | } 78 | if (sort) { 79 | const sortOrders = sort.map(prop => 80 | typeof prop === 'object' 81 | ? Object.entries(prop)[0] 82 | : // Default sort order is 'asc' 83 | [prop, 'asc'] 84 | ); 85 | docs.sort((a, b) => { 86 | for (const [prop, order] of sortOrders) { 87 | const result = collate(a[prop], b[prop]); 88 | if (result !== 0) { 89 | return order === 'asc' ? result : -result; 90 | } 91 | } 92 | return 0; 93 | }); 94 | } 95 | const sortedIndex = docs.findIndex(({ _id }) => doc._id === _id); 96 | // Document update, new place is supposed to be last, `limit` option is set and limit was reached? 97 | if (found && sortedIndex + 1 === limit) { 98 | // Get the actual last document. 99 | const { 100 | docs: [lastDoc] 101 | } = await db.find({ 102 | ...options, 103 | limit: 1, 104 | skip: (options.skip || 0) + sortedIndex 105 | }); 106 | if (lastDoc?._id !== doc._id) { 107 | docs[sortedIndex] = lastDoc; 108 | } 109 | } 110 | // `skip` option is set and document is supposed to be first? 111 | if (skip && sortedIndex === 0) { 112 | // Get the actual first document. 113 | const { 114 | docs: [firstDoc] 115 | } = await db.find({ 116 | ...options, 117 | limit: 1 118 | }); 119 | if (firstDoc?._id !== doc._id) { 120 | docs[0] = firstDoc; 121 | } 122 | } 123 | if (docs.length > limit) { 124 | docs.splice(limit); 125 | } 126 | update(); 127 | } 128 | } 129 | ); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /src/api/useGet.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import stringify from 'fast-json-stable-stringify'; 3 | import reverseArgs from '../utils/reverseArgs'; 4 | import attachmentsAsUint8Arrays from '../attachmentsAsUint8Arrays'; 5 | import changes from '../changes'; 6 | import useDB from '../useDB'; 7 | 8 | const UINT8ARRAY = 'u8a'; 9 | const ALLOWED_LIVE_OPTIONS = ['attachments', 'ajax', 'binary', 'id']; 10 | 11 | async function nextState(binary, doc) { 12 | return binary && doc && !doc._deleted 13 | ? { 14 | ...doc, 15 | _attachments: await attachmentsAsUint8Arrays(doc._attachments) 16 | } 17 | : doc; 18 | } 19 | 20 | export default useListen => 21 | reverseArgs(function useGet(options, db) { 22 | db = useDB(db, { 23 | callee: 'useGet', 24 | example: 'useGet(db, options)' 25 | }); 26 | const { id, attachments, ...otherOptions } = options; 27 | const binary = attachments === UINT8ARRAY; 28 | const optionsWithAttachmentAndBinaryOption = useMemo( 29 | () => ({ 30 | binary, 31 | ...otherOptions, 32 | attachments: !!attachments 33 | }), 34 | [binary, stringify(otherOptions), !!attachments] 35 | ); 36 | const changesOptions = useMemo( 37 | () => ({ 38 | ...optionsWithAttachmentAndBinaryOption, 39 | live: true, 40 | include_docs: true, 41 | since: 'now', 42 | doc_ids: [id] 43 | }), 44 | [optionsWithAttachmentAndBinaryOption, id] 45 | ); 46 | return useListen(db, options, async setValue => { 47 | try { 48 | setValue( 49 | await nextState( 50 | binary, 51 | await db.get(id, optionsWithAttachmentAndBinaryOption) 52 | ) 53 | ); 54 | } catch (error) { 55 | if (error.status !== 404) { 56 | throw error; 57 | } 58 | setValue(null); 59 | } 60 | 61 | // Live? 62 | if ( 63 | Object.keys(options).every(option => 64 | ALLOWED_LIVE_OPTIONS.includes(option) 65 | ) 66 | ) { 67 | return db::changes(changesOptions, async ({ doc }) => { 68 | setValue(await nextState(binary, doc)); 69 | }); 70 | } 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/attachmentsAsUint8Arrays.js: -------------------------------------------------------------------------------- 1 | const blobToUint8Array = blob => 2 | new Promise(resolve => { 3 | const reader = new FileReader(); 4 | reader.onload = () => resolve(new Uint8Array(reader.result)); 5 | reader.readAsArrayBuffer(blob); 6 | }); 7 | 8 | export default typeof global === 'object' && global.Buffer 9 | ? x => x 10 | : async function attachmentsAsUint8Arrays(attachments) { 11 | const result = {}; 12 | for (const [name, { data }] of Object.entries(attachments)) { 13 | result[name] = await blobToUint8Array(data); 14 | } 15 | return result; 16 | }; 17 | -------------------------------------------------------------------------------- /src/changes.js: -------------------------------------------------------------------------------- 1 | import createStore from './utils/createStore'; 2 | import processQueue from './utils/processQueue'; 3 | 4 | const store = createStore(); 5 | 6 | export default function changes(options, handleChange) { 7 | const [eventEmitter, cleanup] = store([this, options], () => { 8 | const eventEmitter = this.changes(options); 9 | return [ 10 | eventEmitter, 11 | eventEmitter => { 12 | eventEmitter.cancel(); 13 | } 14 | ]; 15 | }); 16 | const handleChangeQueued = processQueue(handleChange); 17 | eventEmitter.on('change', handleChangeQueued); 18 | return function cancel() { 19 | eventEmitter.removeListener('change', handleChangeQueued); 20 | cleanup(); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/concurrent/index.js: -------------------------------------------------------------------------------- 1 | // @codegen 2 | require('../../api')(module); 3 | -------------------------------------------------------------------------------- /src/concurrent/useListen.js: -------------------------------------------------------------------------------- 1 | import useSubscriptionSuspense from '../utils/useSubscriptionSuspense'; 2 | import createListenHook from '../createListenHook'; 3 | 4 | export default createListenHook(useSubscriptionSuspense); 5 | -------------------------------------------------------------------------------- /src/createListenHook.js: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import handleError from './handleError'; 3 | import useGetSubscription from './useGetSubscription'; 4 | import debounceSetValue from './debounceSetValue'; 5 | 6 | export default useListen => (db, options, subscribe) => 7 | useGetSubscription( 8 | db, 9 | options, 10 | useCallback(setValue => setValue |> debounceSetValue(db) |> subscribe, [ 11 | db, 12 | subscribe 13 | ]) 14 | ) 15 | |> (subscription => useListen(subscription, db)) 16 | |> handleError; 17 | -------------------------------------------------------------------------------- /src/createPouchDB.js: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb'; 2 | 3 | export default function createPouchDB({ 4 | // Time to wait for suspended component to actually mount and subscribe. 5 | synchronousAPITemporarySubscriptionCleanupDelay, 6 | // Limit excessive re-rendering from bulkDocs: 7 | // Time to wait more changes. Final update is made after this, if there were more updates. 8 | // Set null to disable debouncing updates. 9 | debounceUpdatesWait, 10 | // Limit update frequency. 11 | debounceUpdatesMaxWait, 12 | maxListeners, 13 | ...options 14 | }) { 15 | const db = new PouchDB( 16 | // PouchDB constructor modifies the options object. Make sure options is a 17 | // copy so the original object remains untouched. 18 | options 19 | ); 20 | if (maxListeners) { 21 | db.setMaxListeners(maxListeners); 22 | } 23 | return Object.assign(db, { 24 | reactPouchDBOptions: { 25 | synchronousAPITemporarySubscriptionCleanupDelay, 26 | debounceUpdatesWait, 27 | debounceUpdatesMaxWait 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/createSubscription.js: -------------------------------------------------------------------------------- 1 | export default function createSubscription(subscribe, remove) { 2 | let value; 3 | let subscribing; 4 | const updaters = new Set(); 5 | return { 6 | getCurrentValue: () => value, 7 | subscribe: update => { 8 | updaters.add(update); 9 | if (updaters.size === 1) { 10 | subscribing = (async () => { 11 | try { 12 | return await subscribe(nextValue => { 13 | value = [null, nextValue]; 14 | updaters.forEach(updater => updater()); 15 | }); 16 | } catch (error) { 17 | value = [error]; 18 | updaters.forEach(updater => updater()); 19 | } 20 | })(); 21 | } 22 | 23 | return async () => { 24 | updaters.delete(update); 25 | if (!updaters.size) { 26 | remove(); 27 | const unsubscribe = await subscribing; 28 | unsubscribe?.(); 29 | } 30 | }; 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/debounceSetValue.js: -------------------------------------------------------------------------------- 1 | import { debounce } from 'lodash'; 2 | 3 | export default ({ 4 | reactPouchDBOptions, 5 | reactPouchDBOptions: { 6 | debounceUpdatesWait: wait = 100, 7 | debounceUpdatesMaxWait: maxWait = 1000 8 | } 9 | }) => 10 | reactPouchDBOptions.debounceUpdatesWait === null 11 | ? x => x 12 | : setValue => 13 | // For batch of changes from bulkDocs, update only every `maxWait`. 14 | // If there were more updates, update also finally after `wait`. 15 | debounce(setValue, wait, { 16 | leading: true, 17 | trailing: true, 18 | maxWait 19 | }); 20 | -------------------------------------------------------------------------------- /src/getSubscription.js: -------------------------------------------------------------------------------- 1 | import createStore from './utils/createStore'; 2 | import createSubscription from './createSubscription'; 3 | 4 | const store = createStore(); 5 | 6 | export default function getSubscription(db, key, subscribe) { 7 | return store([db, key], remove => [createSubscription(subscribe, remove)])[0]; 8 | } 9 | -------------------------------------------------------------------------------- /src/handleError.js: -------------------------------------------------------------------------------- 1 | export default function handleError([error, data] = []) { 2 | if (error) { 3 | throw error; 4 | } 5 | return data; 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @codegen 2 | require('../api')(module); 3 | -------------------------------------------------------------------------------- /src/renderProps.js: -------------------------------------------------------------------------------- 1 | import { Children, cloneElement, createElement } from 'react'; 2 | import useDB from './useDB'; 3 | 4 | export default (valueToProps, error) => useAPI => { 5 | const useAPIAndHandleReturnValue = (...args) => 6 | useAPI(...args) |> valueToProps; 7 | 8 | return function useRenderProps({ db, children, ...otherProps }) { 9 | const props = { 10 | db: useDB(db, error) 11 | }; 12 | 13 | Object.assign( 14 | props, 15 | db 16 | ? useAPIAndHandleReturnValue(db, otherProps) 17 | : useAPIAndHandleReturnValue(otherProps) 18 | ); 19 | 20 | if (typeof children === 'function') { 21 | return children.prototype?.isReactComponent 22 | ? createElement(children, props) 23 | : children(props); 24 | } 25 | 26 | return cloneElement(Children.only(children), props); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/useDB.js: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo, useEffect } from 'react'; 2 | import PouchDB from 'pouchdb'; 3 | import stringify from 'fast-json-stable-stringify'; 4 | import createStore from './utils/createStore'; 5 | import DBContext from './DBContext'; 6 | import createPouchDB from './createPouchDB'; 7 | 8 | const store = createStore(); 9 | 10 | export function useDBOptions(options) { 11 | const optionsObject = 12 | typeof options === 'string' ? { name: options } : options; 13 | const key = stringify(optionsObject); 14 | const optionsMemoized = useMemo(() => optionsObject, [key]); 15 | const dependencies = [optionsMemoized]; 16 | const [value, cleanup] = useMemo( 17 | () => 18 | key === undefined 19 | ? [] 20 | : store([key], () => [ 21 | optionsMemoized ? createPouchDB(optionsMemoized) : undefined, 22 | value => { 23 | value?.close(); 24 | } 25 | ]), 26 | dependencies 27 | ); 28 | useEffect(() => cleanup, dependencies); 29 | return value; 30 | } 31 | 32 | export function useDBContext() { 33 | return useContext(DBContext); 34 | } 35 | 36 | export default function useDB( 37 | any, 38 | { callee = 'useDB', example = 'useDB(options)' } = {} 39 | ) { 40 | const dbInstance = any instanceof PouchDB ? any : undefined; 41 | const options = dbInstance ? undefined : any; 42 | const dbOptions = useDBOptions(options); 43 | const dbContext = useDBContext(); 44 | 45 | const db = dbInstance ?? dbOptions ?? dbContext; 46 | 47 | if (!db) { 48 | throw new Error( 49 | callee 50 | ? `\`${callee}\` was called without \`db\` and database is not in context. Provide database using or \`${example}\`` 51 | : 'Database is not in context. Provide database using ' 52 | ); 53 | } 54 | return db; 55 | } 56 | -------------------------------------------------------------------------------- /src/useGetSubscription.js: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import stringify from 'fast-json-stable-stringify'; 3 | import getSubscription from './getSubscription'; 4 | 5 | export default function useGetSubscription(db, options, subscribe) { 6 | const key = stringify(options); 7 | return useMemo(() => getSubscription(db, key, subscribe), [ 8 | db, 9 | key, 10 | subscribe 11 | ]); 12 | } 13 | -------------------------------------------------------------------------------- /src/useListen.js: -------------------------------------------------------------------------------- 1 | import useSubscriptionImmediateSuspense from './utils/useSubscriptionImmediateSuspense'; 2 | import createListenHook from './createListenHook'; 3 | 4 | export default createListenHook( 5 | ( 6 | subscription, 7 | { reactPouchDBOptions: { synchronousAPITemporarySubscriptionCleanupDelay } } 8 | ) => 9 | useSubscriptionImmediateSuspense( 10 | subscription, 11 | synchronousAPITemporarySubscriptionCleanupDelay 12 | ) 13 | ); 14 | -------------------------------------------------------------------------------- /src/utils/createStore.js: -------------------------------------------------------------------------------- 1 | // import get from '@postinumero/map-get-with-default'; 2 | const get = function(key, getDefault) { 3 | return this.has(key) 4 | ? this.get(key) 5 | : (() => { 6 | const defaultValue = getDefault(); 7 | this.set(key, defaultValue); 8 | return defaultValue; 9 | })(); 10 | }; 11 | 12 | export default function createStore() { 13 | const root = new Map(); 14 | return (path, create) => { 15 | function remove({ onCleanup, value }) { 16 | onCleanup?.(value); 17 | let parent = root; 18 | const chain = path.map(key => { 19 | const item = { parent, key }; 20 | parent = parent.get(key); 21 | return item; 22 | }); 23 | (function removeChild() { 24 | const { parent, key } = chain.pop(); 25 | parent.delete(key); 26 | if (!parent.size && chain.length) { 27 | removeChild(); 28 | } 29 | })(); 30 | } 31 | const pathCopy = [...path]; 32 | const lastKey = pathCopy.pop(); 33 | const leaf = pathCopy.reduce( 34 | (parent, key) => parent::get(key, () => new Map()), 35 | root 36 | ); 37 | const item = leaf::get(lastKey, () => { 38 | const [value, onCleanup] = create(() => remove(item)); 39 | const item = { 40 | value, 41 | onCleanup, 42 | referenceCounter: 0 43 | }; 44 | return item; 45 | }); 46 | item.referenceCounter = item.referenceCounter + 1; 47 | return [ 48 | item.value, 49 | function cleanup() { 50 | item.referenceCounter = item.referenceCounter - 1; 51 | if (!item.referenceCounter) { 52 | remove(item); 53 | } 54 | } 55 | ]; 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/processQueue.js: -------------------------------------------------------------------------------- 1 | export default fn => { 2 | let processing; 3 | return async function queued(...args) { 4 | // 2.b. Overwrite current processing indicator with a promise that resolves after current processing has completed 5 | processing = (async () => { 6 | // 1. Wait until possible previous process has been resolved 7 | await processing; 8 | // 2.a. Process this request and return response 9 | return this::fn(...args); 10 | })(); 11 | // 3. Return promise that resolves to response from fn call with current args 12 | return processing; 13 | }; 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/reverseArgs.js: -------------------------------------------------------------------------------- 1 | export default fn => 2 | function reverseArgs(...args) { 3 | return this::fn(...args.reverse()); 4 | }; 5 | -------------------------------------------------------------------------------- /src/utils/useSubscriptionImmediateSuspense.js: -------------------------------------------------------------------------------- 1 | import { useSubscription } from 'use-subscription'; 2 | 3 | // Like use-subscription, but if current value is undefined, this will suspend immediately until value is received. 4 | // Initial subscription is closed after cleanupDelay. Component should have mounted (and subscribed) by then or process is repeated indefinitely. 5 | export default function useSubscriptionImmediateSuspense( 6 | subscription, 7 | cleanupDelay = 30000 8 | ) { 9 | if (!subscription.getCurrentValue()) { 10 | throw new Promise(resolve => { 11 | const unsubscribe = subscription.subscribe(resolve); 12 | setTimeout(unsubscribe, cleanupDelay); 13 | }); 14 | } 15 | return useSubscription(subscription); 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/useSubscriptionSuspense.js: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useCallback } from 'react'; 2 | import { useSubscription } from 'use-subscription'; 3 | 4 | // Like use-subscription, but if current value is undefined, this will return undefined, and suspend right after first render until value is received. 5 | export default function useSubscriptionSuspense(subscription) { 6 | const initializing = useRef(true); 7 | const suspender = useRef(); 8 | useEffect(() => { 9 | initializing.current = false; 10 | }, []); 11 | return useSubscription({ 12 | ...subscription, 13 | getCurrentValue: useCallback(() => { 14 | const value = subscription.getCurrentValue(); 15 | if (value !== undefined) { 16 | return value; 17 | } 18 | if (!suspender.current) { 19 | suspender.current = new Promise(resolve => { 20 | const unsubscribe = subscription.subscribe( 21 | function checkForUpdates() { 22 | resolve(); 23 | unsubscribe(); 24 | } 25 | ); 26 | }); 27 | } 28 | if (initializing.current) { 29 | return; 30 | } 31 | throw suspender.current; 32 | }, [subscription]) 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/withDB.js: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react'; 2 | import hoistStatics from 'hoist-non-react-statics'; 3 | import reverseArgs from './utils/reverseArgs'; 4 | import useDB from './useDB'; 5 | 6 | export default reverseArgs(function withDB(Component, db) { 7 | return forwardRef( 8 | hoistStatics( 9 | Object.assign( 10 | (props, ref) => ( 11 | 19 | ), 20 | { 21 | displayName: `withDB(${Component.displayName || Component.name})`, 22 | WrappedComponent: Component 23 | } 24 | ), 25 | Component 26 | ) 27 | ); 28 | }); 29 | -------------------------------------------------------------------------------- /testapp/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | SKIP_PREFLIGHT_CHECK=true 3 | -------------------------------------------------------------------------------- /testapp/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./node_modules/@visma/create-react-app-template/eslintrc.json"] 3 | } 4 | -------------------------------------------------------------------------------- /testapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /testapp/README.md: -------------------------------------------------------------------------------- 1 | This project uses [@visma/create-react-app-template](https://www.npmjs.com/package/@visma/create-react-app-template). 2 | -------------------------------------------------------------------------------- /testapp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@visma/create-react-app-template/cjs/babel-preset'] 3 | }; 4 | -------------------------------------------------------------------------------- /testapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pouchdb-testapp", 3 | "version": "2.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@arnosaine/react-scripts": "^4.0.3-0", 7 | "assert": "^2.0.0", 8 | "pouchdb-browser": "^7.1.1", 9 | "react": "^16.10.2", 10 | "react-app-polyfill": "^1.0.4", 11 | "react-dom": "^16.10.2", 12 | "react-error-boundary": "^1.2.5", 13 | "react-pouchdb": "^2.1.0" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "dotenv-webpack": "^1.7.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /testapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | Test React PouchDB 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /testapp/src/App.js: -------------------------------------------------------------------------------- 1 | import { PouchDB } from 'react-pouchdb/browser'; 2 | import DestroyDB from 'DestroyDB'; 3 | import TestSequence from './TestSequence'; 4 | import Tests from './Tests'; 5 | 6 | const dbName = 'test'; 7 | 8 | function App() { 9 | return ( 10 | <> 11 |

Test React PouchDB

12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /testapp/src/TestSequence.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import sleep from 'sleep'; 3 | import { useDB } from 'react-pouchdb/browser'; 4 | import { config } from 'Test'; 5 | 6 | export default function TestSequence({ children }) { 7 | const db = useDB(); 8 | const [initialized, setInitialized] = useState(false); 9 | useEffect(() => { 10 | (async () => { 11 | let a = await db.put({ _id: 'a', value: 'created' }); 12 | setInitialized(true); 13 | await sleep(config.sleep); 14 | let b = await db.put({ _id: 'b', value: 'created' }); 15 | await sleep(config.sleep); 16 | a = await db.put({ _id: 'a', _rev: a.rev, value: 'update' }); 17 | b = await db.put({ _id: 'b', _rev: b.rev, value: 'update' }); 18 | await sleep(config.sleep); 19 | await db.remove({ _id: 'a', _rev: a.rev }); 20 | await db.remove({ _id: 'b', _rev: b.rev }); 21 | })(); 22 | }, [db]); 23 | return initialized ? children : null; 24 | } 25 | -------------------------------------------------------------------------------- /testapp/src/Tests/AllDocuments.js: -------------------------------------------------------------------------------- 1 | import { useTestRender, SynchronousAndConcurrentAPIs } from 'Test'; 2 | 3 | export default function AllDocuments({ singleKey, keys, ...otherProps }) { 4 | return ( 5 | 6 | {api => } 7 | 8 | ); 9 | } 10 | 11 | function Test({ api: { useAllDocs }, singleKey, ...otherProps }) { 12 | const docs = useAllDocs({ key: singleKey, ...otherProps }); 13 | return useTestRender( 14 | docs 15 | ? docs.length 16 | ? docs[0].doc 17 | ? docs[0].doc.value 18 | : 'deleted' 19 | : 'empty array' 20 | : docs 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /testapp/src/Tests/DontSwallowErrors.js: -------------------------------------------------------------------------------- 1 | import { useTestRender, SynchronousAndConcurrentAPIs } from 'Test'; 2 | 3 | export default function DontSwallowErrors() { 4 | return ( 5 | 12 | {api => } 13 | 14 | ); 15 | } 16 | 17 | function Test({ api: { useFind } }) { 18 | return useTestRender( 19 | useFind({ selector: { age: null }, sort: ['firstName'] }) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /testapp/src/Tests/FindDocument.js: -------------------------------------------------------------------------------- 1 | import { useTestRender, SynchronousAndConcurrentAPIs } from 'Test'; 2 | 3 | export default function FindDocument({ selector, ...otherProps }) { 4 | return ( 5 | 6 | {api => } 7 | 8 | ); 9 | } 10 | 11 | function Test({ api: { useFind }, ...otherProps }) { 12 | const docs = useFind(otherProps); 13 | return useTestRender( 14 | docs ? (docs.length ? docs[0].value : 'empty array') : docs 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /testapp/src/Tests/GetDocument.js: -------------------------------------------------------------------------------- 1 | import { useTestRender, SynchronousAndConcurrentAPIs } from 'Test'; 2 | 3 | export default function GetDocument({ id, ...otherProps }) { 4 | return ( 5 | 6 | {api => } 7 | 8 | ); 9 | } 10 | 11 | function Test({ api: { useGet }, ...otherProps }) { 12 | const doc = useGet(otherProps); 13 | return useTestRender(doc ? (doc._deleted ? 'deleted' : doc.value) : doc); 14 | } 15 | -------------------------------------------------------------------------------- /testapp/src/Tests/index.js: -------------------------------------------------------------------------------- 1 | import { config } from 'Test'; 2 | import FindDocument from './FindDocument'; 3 | import GetDocument from './GetDocument'; 4 | import AllDocuments from './AllDocuments'; 5 | import DontSwallowErrors from './DontSwallowErrors'; 6 | 7 | export default function Tests() { 8 | return ( 9 | <> 10 | {config.get && config.existing && ( 11 | 25 | )} 26 | {config.get && config.missing && ( 27 | 42 | )} 43 | {config.allDocs && config.existing && ( 44 | 58 | )} 59 | {config.allDocs && config.missing && ( 60 | 75 | )} 76 | {config.allDocs && config.existing && ( 77 | 91 | )} 92 | {config.allDocs && config.missing && ( 93 | 114 | )} 115 | {config.find && config.existing && ( 116 | 130 | )} 131 | {config.find && config.missing && ( 132 | 153 | )} 154 | {config.dontSwallowErrors && } 155 | 156 | ); 157 | } 158 | -------------------------------------------------------------------------------- /testapp/src/index.js: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import 'react-app-polyfill/stable'; 3 | import ReactDOM from 'react-dom'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /testapp/src/shared/DestroyDB.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import PouchDB from 'pouchdb-browser'; 3 | 4 | export default function DestroyDB({ name, children }) { 5 | const [destroyed, setDestroyed] = useState(false); 6 | useEffect(() => { 7 | (async () => { 8 | let db = new PouchDB(name); 9 | await db.destroy(); 10 | setDestroyed(true); 11 | })(); 12 | }, [name]); 13 | return destroyed ? children : null; 14 | } 15 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/Context.js: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | const Context = createContext(); 4 | 5 | export default Context; 6 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/ErrorBoundaryAndSuspenseOrder/ErrorBoundary.js: -------------------------------------------------------------------------------- 1 | import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary'; 2 | import { useTestRender } from '..'; 3 | 4 | function Fallback() { 5 | return useTestRender('error'); 6 | } 7 | 8 | export default function ErrorBoundary({ children }) { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/ErrorBoundaryAndSuspenseOrder/Suspense.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTestRender } from '..'; 3 | 4 | export default function Suspense({ children }) { 5 | return }>{children}; 6 | } 7 | 8 | function Fallback() { 9 | return useTestRender('loading'); 10 | } 11 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/ErrorBoundaryAndSuspenseOrder/index.js: -------------------------------------------------------------------------------- 1 | import Test, { config } from '..'; 2 | import ErrorBoundary from './ErrorBoundary'; 3 | import Suspense from './Suspense'; 4 | 5 | export default function ErrorBoundaryAndSuspenseOrder({ 6 | children, 7 | message, 8 | ...otherProps 9 | }) { 10 | return ( 11 | <> 12 | {config.suspenseBeforeErrorBoundary && ( 13 | ErrorBoundary: ${message}`} {...otherProps}> 14 | 15 | {children} 16 | 17 | 18 | )} 19 | {config.errorBoundaryBeforeSuspense && ( 20 | Suspense: ${message}`} {...otherProps}> 21 | 22 | {children} 23 | 24 | 25 | )} 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/SynchronousAndConcurrentAPIs.js: -------------------------------------------------------------------------------- 1 | import * as synchronous from 'react-pouchdb/browser'; 2 | import * as concurrent from 'react-pouchdb/browser/concurrent'; 3 | import { ErrorBoundaryAndSuspenseOrder, config } from '.'; 4 | import { deepStrictEqual } from 'assert'; 5 | 6 | export default function SynchronousAndConcurrentModes({ 7 | message, 8 | expected, 9 | children 10 | }) { 11 | return ( 12 | <> 13 | {config.concurrent && ( 14 | deepStrictEqual(actual, expected.concurrent)} 17 | > 18 | {children(concurrent)} 19 | 20 | )} 21 | {config.synchronous && ( 22 | deepStrictEqual(actual, expected.synchronous)} 25 | > 26 | {children(synchronous)} 27 | 28 | )} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "get": true, 3 | "allDocs": true, 4 | "find": true, 5 | "existing": true, 6 | "missing": true, 7 | "synchronous": true, 8 | "concurrent": true, 9 | "errorBoundaryBeforeSuspense": true, 10 | "suspenseBeforeErrorBoundary": true, 11 | "dontSwallowErrors": true, 12 | "sleep": 200 13 | } 14 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/index.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | import Context from './Context'; 3 | 4 | export default function Test({ message, test, children }) { 5 | const [actual, setActual] = useState([]); 6 | 7 | return ( 8 | setActual(actual => [...actual, value]), [])} 10 | > 11 |

{message}

12 |
13 |         {JSON.stringify(
14 |           useMemo(() => {
15 |             try {
16 |               test(actual);
17 |               return 'ok';
18 |             } catch ({ name, message, generatedMessage, ...other }) {
19 |               return other;
20 |             }
21 |           }, [actual, test]),
22 |           null,
23 |           2
24 |         )}
25 |       
26 | {children} 27 |
28 | ); 29 | } 30 | 31 | export ErrorBoundaryAndSuspenseOrder from './ErrorBoundaryAndSuspenseOrder'; 32 | export SynchronousAndConcurrentAPIs from './SynchronousAndConcurrentAPIs'; 33 | export useTestRender from './useTestRender'; 34 | export config from './config'; 35 | -------------------------------------------------------------------------------- /testapp/src/shared/Test/useTestRender.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import Context from './Context'; 3 | 4 | export default function useTestRender(value) { 5 | useContext(Context)( 6 | value 7 | ? value 8 | : value === null 9 | ? 'null' 10 | : value === undefined 11 | ? 'undefined' 12 | : 'test failure' 13 | ); 14 | return null; 15 | } 16 | -------------------------------------------------------------------------------- /testapp/src/shared/sleep.js: -------------------------------------------------------------------------------- 1 | export default ms => new Promise(resolve => setTimeout(resolve, ms)); 2 | -------------------------------------------------------------------------------- /testapp/webpack.config.js: -------------------------------------------------------------------------------- 1 | import Dotenv from 'dotenv-webpack'; 2 | import path from 'path'; 3 | 4 | export default env => config => { 5 | const { 6 | plugins, 7 | resolve: { alias, modules } 8 | } = config; 9 | 10 | alias.react = path.resolve('./node_modules/react'); 11 | 12 | plugins.push(new Dotenv()); 13 | 14 | modules.push('shared'); 15 | 16 | return config; 17 | }; 18 | -------------------------------------------------------------------------------- /todoapp/.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | SKIP_PREFLIGHT_CHECK=true 3 | -------------------------------------------------------------------------------- /todoapp/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["./node_modules/@visma/create-react-app-template/eslintrc.json"] 3 | } 4 | -------------------------------------------------------------------------------- /todoapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /todoapp/README.md: -------------------------------------------------------------------------------- 1 | This project uses [@visma/create-react-app-template](https://www.npmjs.com/package/@visma/create-react-app-template). 2 | -------------------------------------------------------------------------------- /todoapp/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['@visma/create-react-app-template/cjs/babel-preset'] 3 | }; 4 | -------------------------------------------------------------------------------- /todoapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pouchdb-todoapp", 3 | "version": "2.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@arnosaine/react-scripts": "^4.0.3-0", 7 | "classnames": "^2.2.6", 8 | "pouchdb-browser": "^7.1.1", 9 | "react": "^16.10.2", 10 | "react-dom": "^16.10.2", 11 | "react-pouchdb": "^2.1.0", 12 | "react-router-dom": "^5.1.2", 13 | "todomvc-app-css": "^2.3.0", 14 | "todomvc-common": "^1.0.5" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "browserslist": [ 23 | ">0.2%", 24 | "not dead", 25 | "not ie <= 11", 26 | "not op_mini all" 27 | ], 28 | "homepage": "/react-pouchdb" 29 | } 30 | -------------------------------------------------------------------------------- /todoapp/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /todoapp/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 16 | 25 | Todo React PouchDB 26 | 64 | 65 | 66 | 67 |
68 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /todoapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "theme_color": "#000000", 7 | "background_color": "#ffffff" 8 | } 9 | -------------------------------------------------------------------------------- /todoapp/src/App.js: -------------------------------------------------------------------------------- 1 | import { Suspense } from 'react'; 2 | import { BrowserRouter } from 'react-router-dom'; 3 | import { PouchDB } from 'react-pouchdb/browser'; 4 | import Container from './Container'; 5 | import Footer from './Footer'; 6 | import Input from './Input'; 7 | import List from './List'; 8 | import ToggleAll from './ToggleAll'; 9 | import { homepage } from '../package.json'; 10 | 11 | const basename = process.env.NODE_ENV === 'development' ? undefined : homepage; 12 | 13 | function App() { 14 | return ( 15 | 16 | 17 | 18 | 19 |
20 |
21 |

todos

22 |
23 | 24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ); 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /todoapp/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /todoapp/src/Container/Aside.js: -------------------------------------------------------------------------------- 1 | export default () => ( 2 | 33 | ); 34 | -------------------------------------------------------------------------------- /todoapp/src/Container/index.js: -------------------------------------------------------------------------------- 1 | import Aside from './Aside'; 2 | import styles from './styles.module.css'; 3 | 4 | export default ({ children }) => ( 5 |
6 |
9 | ); 10 | -------------------------------------------------------------------------------- /todoapp/src/Container/styles.module.css: -------------------------------------------------------------------------------- 1 | @media (min-width: 899px) { 2 | .learn-bar { 3 | margin-left: 150px; 4 | margin-right: -150px; 5 | padding-left: 0; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /todoapp/src/Footer/ClearCompleted.js: -------------------------------------------------------------------------------- 1 | import { useDB, useFind } from 'react-pouchdb/browser'; 2 | 3 | export default function ClearCompleted() { 4 | const docs = useFind({ 5 | selector: { 6 | completed: true 7 | } 8 | }); 9 | const { bulkDocs } = useDB(); 10 | const { length } = docs; 11 | return length ? ( 12 | 18 | ) : null; 19 | } 20 | -------------------------------------------------------------------------------- /todoapp/src/Footer/Counter.js: -------------------------------------------------------------------------------- 1 | import { useFind } from 'react-pouchdb/browser'; 2 | 3 | export default function Counter() { 4 | const docs = useFind({ 5 | selector: { 6 | completed: { $ne: true } 7 | } 8 | }); 9 | const { length } = docs; 10 | return ( 11 | 12 | {length} {length === 1 ? 'item' : 'items'} left 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /todoapp/src/Footer/Filter.js: -------------------------------------------------------------------------------- 1 | import { NavLink } from 'react-router-dom'; 2 | 3 | export default function Filter() { 4 | return ( 5 |
    6 | {[ 7 | { path: '', title: 'All' }, 8 | { path: 'active', title: 'Active' }, 9 | { path: 'completed', title: 'Completed' } 10 | ].map(({ path, title }) => ( 11 |
  • 12 | 13 | {title} 14 | 15 |
  • 16 | ))} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /todoapp/src/Footer/index.js: -------------------------------------------------------------------------------- 1 | import { useFind } from 'react-pouchdb/browser'; 2 | import ClearCompleted from './ClearCompleted'; 3 | import Counter from './Counter'; 4 | import Filter from './Filter'; 5 | 6 | export default function Footer() { 7 | const docs = useFind({ 8 | selector: {} 9 | }); 10 | const { length } = docs; 11 | return length ? ( 12 |
13 | 14 | 15 | 16 |
17 | ) : null; 18 | } 19 | -------------------------------------------------------------------------------- /todoapp/src/Input.js: -------------------------------------------------------------------------------- 1 | import { useDB } from 'react-pouchdb/browser'; 2 | 3 | export default function Input() { 4 | const { post } = useDB(); 5 | return ( 6 | { 10 | const title = target.value.trim(); 11 | if (key === 'Enter' && title) { 12 | post({ title, timestamp: Date.now() }); 13 | target.value = ''; 14 | } 15 | }} 16 | placeholder="What needs to be done?" 17 | type="text" 18 | /> 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /todoapp/src/List/Docs.js: -------------------------------------------------------------------------------- 1 | import { useFind } from 'react-pouchdb/browser'; 2 | import Item from './Item'; 3 | 4 | const filterByCompletedField = { 5 | active: { $ne: true }, 6 | completed: true 7 | }; 8 | 9 | export default function Docs({ 10 | match: { 11 | params: { filter } 12 | } 13 | }) { 14 | const docs = useFind({ 15 | selector: { 16 | timestamp: { $gte: null }, 17 | completed: filterByCompletedField[filter] 18 | }, 19 | sort: ['timestamp'] 20 | }); 21 | return ( 22 |
    23 | {docs.map(doc => ( 24 | 25 | ))} 26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /todoapp/src/List/Item.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import classNames from 'classnames'; 3 | import { useDB } from 'react-pouchdb/browser'; 4 | 5 | export default function Item({ doc, doc: { completed = false, title } }) { 6 | const { put, remove } = useDB(); 7 | const [focus, setFocus] = useState(); 8 | const [isEditing, setIsEditing] = useState(); 9 | const inputRef = useRef(); 10 | 11 | useEffect(() => { 12 | if (focus) { 13 | inputRef.current.focus(); 14 | setFocus(false); 15 | } 16 | }, [focus, setFocus]); 17 | 18 | function handleSave() { 19 | if (isEditing) { 20 | setIsEditing(false); 21 | put({ 22 | ...doc, 23 | title: inputRef.current.value.trim() 24 | }); 25 | } 26 | } 27 | 28 | return ( 29 |
  • { 32 | setFocus(true); 33 | setIsEditing(true); 34 | }} 35 | > 36 |
    37 | 41 | put({ 42 | ...doc, 43 | completed: !completed 44 | }) 45 | } 46 | type="checkbox" 47 | /> 48 | 49 |
    51 | { 57 | if (key === 'Enter') { 58 | handleSave(); 59 | } 60 | }} 61 | onBlur={handleSave} 62 | /> 63 |
  • 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /todoapp/src/List/index.js: -------------------------------------------------------------------------------- 1 | import { Route } from 'react-router-dom'; 2 | import Docs from './Docs'; 3 | 4 | export default function List() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /todoapp/src/ToggleAll.js: -------------------------------------------------------------------------------- 1 | import { useDB, useFind } from 'react-pouchdb/browser'; 2 | 3 | export default function ToggleAll() { 4 | const docs = useFind({ 5 | selector: {} 6 | }); 7 | const { bulkDocs } = useDB(); 8 | return docs.length 9 | ? (() => { 10 | const completed = docs.every(({ completed }) => completed); 11 | return ( 12 | <> 13 | 18 | bulkDocs(docs.map(doc => ({ ...doc, completed: !completed }))) 19 | } 20 | type="checkbox" 21 | /> 22 |