UNPKG

@jameshclrk/rxdb-hooks

Version:

React hooks for integrating with RxDB

464 lines (350 loc) 14.1 kB
# rxdb-hooks #### React hooks for integrating with [RxDB](https://github.com/pubkey/rxdb) <a href="https://www.npmjs.com/package/rxdb-hooks"> <img src="https://img.shields.io/npm/v/rxdb-hooks?color=%23E6008D&style=flat-square" alt="npm version"> </a> <a href="https://circleci.com/gh/cvara/rxdb-hooks/tree/master"> <img src="https://img.shields.io/circleci/build/github/cvara/rxdb-hooks/master?style=flat-square" /> </a> <a href="https://codecov.io/gh/cvara/rxdb-hooks"> <img src="https://img.shields.io/codecov/c/github/cvara/rxdb-hooks?style=flat-square" /> </a> <a href="https://www.npmjs.com/package/rxdb-hooks"> <img src="https://img.shields.io/npm/dm/rxdb-hooks?color=%233498db&style=flat-square" alt="downloads"> </a> ## Table of Contents <details> <summary>Click to expand</summary> - [About](#about) - [Installation](#installation) - [Example](#example) - [Compatibility with RxDB](#compatibility-with-rxdb) - [Migration Guide](#migration-guide) - [API](#api) - [`Provider`](#provider) - [`useRxDB`](#userxdb) - [`useRxCollection`](#userxcollection) - [`useRxQuery`](#userxquery) - [`useRxData`](#userxdata) - [`useRxDocument`](#userxdocument) - [Recipes](#recipes) - [Query and Query Constructor memoization](#query-and-query-constructor-memoization) - [Lazy instantiation of RxDatabase & RxCollections](#lazy-instantiation-of-rxdatabase--rxcollections) - [Mutations](#mutations) - [LICENSE](#license) </details> ## About Nothing fancy, just conveniently handles common use cases such as: - subscribing to query observables and translating results into React state - cleaning up after subscriptions where necessary - paginating results - maintaining useful state information (i.e. data fetching or data exhaustion during pagination) - lazily creating or destroying collections ## Installation ```bash # using npm npm install rxdb-hooks # using yarn yarn add rxdb-hooks ``` ## Example **Root.jsx**: ```javascript import React, { useEffect } from 'react'; import { Provider } from 'rxdb-hooks'; import initialize from './initialize'; const Root = () => { const [db, setDb] = useState(); useEffect(() => { // RxDB instantiation can be asynchronous initialize().then(setDb); }, []); // Until db becomes available, consumer hooks that // depend on it will still work, absorbing the delay // by setting their state to isFetching:true return ( <Provider db={db}> <App /> </Provider> ); }; ``` **Consumer.jsx**: ```javascript import React from 'react'; import { useRxData } from 'rxdb-hooks'; const Consumer = () => { const { result: characters, isFetching } = useRxData( // the collection to be queried 'characters', // a function returning the query to be applied collection => collection.find({ selector: { affiliation: 'jedi', }, }) ); if (isFetching) { return 'loading characters...'; } return ( <ul> {characters.map((character, idx) => ( <li key={idx}>{character.name}</li> ))} </ul> ); }; ``` **initialize.js**: ```javascript const initialize = async () => { // create RxDB const db = await createRxDatabase({ name: 'test_database', }); // create a collection const collection = await db.addCollections({ characters: { schema: { title: 'characters', version: 0, type: 'object', primaryKey: 'id', properties: { id: { type: 'string', maxLength: 100, }, name: { type: 'string', }, }, }, }, }); // maybe sync collection to a remote // ... return db; }; ``` ## Compatibility with RxDB The core API of rxdb-hooks remains largely the same across all major versions _beyond_ `1.x`, however some parts of the internal implementation (most notably [the plugin](src/plugins.ts)) differ based on the version of rxdb we need to target **\***. Please use the appropriate version of rxdb-hooks as per this table: | rxdb-hooks version | targeted RxDB version | | ------------------ | ---------------------- | | `5.x` | `14.x` | | `4.1.x` | `13.x` | | `4.0.x` | `10.x`, `11.x`, `12.x` | | `3.x` | `9.x` | | `1.x`, `2.x` | `8.x` | _\* Versions 7.x of RxDB and below have not been tested and are not guaranteed to work with rxdb-hooks_ ## Migration Guide ### `4.x` => `5.x` - `useRxDocument` has been dropped; for fetching single documents simply use `useRxQuery` or `useRxData` - observing lazily created collection has become an opt-in feature that, if needed, has to be explicitly enabled by using the provided plugin. For more info see [Lazy instantiation of RxDatabase & RxCollections](#lazy-instantiation-of-rxdatabase--rxcollections) ## API ### `Provider` The `<Provider />` makes the RxDatabase instance available to nested components and is required for all subsequent hooks to work. #### Props | Property | Type | Description | | -------- | ------------ | -------------------------------------------- | | `db` | `RxDatabase` | the RxDatabase instance to consume data from | <hr /> ### `useRxDB` Returns the RxDatabase instance made available by the `<Provider />` ```javascript function useRxDB(): RxDatabase ``` #### Example ```javascript const db = useRxDB(); ``` <hr /> ### `useRxCollection` Given a collection name returns an RxCollection instance, if found in RxDatabase. ```javascript function useRxCollection<T>(name: string): RxCollection<T> | null ``` #### Example ```javascript const collection = useRxCollection('characters'); ``` <hr /> ### `useRxQuery` Subscribes to given RxQuery object providing query results and some helpful extra state variables. ```javascript function useRxQuery<T>(query: RxQuery, options?: UseRxQueryOptions): RxQueryResult<T> ``` #### `options: UseRxQueryOptions` | Option | Type | Description | | ------------ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pageSize` | `number` | (optional) enables pagination & defines page limit | | `pagination` | `"Traditional" \| "Infinite"` | (optional) determines pagination mode: <br>`Traditional`: results are split into pages, starts by rendering the first page and total `pageCount` is returned, allowing for requesting results of any specific page. <br>`Infinite`: first page of results is rendered, allowing for gradually requesting more. <br>**Default**: `"Traditional"` | | `json` | `boolean` | (optional) when `true` resulting documents will be converted to plain JavaScript objects; equivalent to manually calling `.toJSON()` on each `RxDocument`. **Default**: `false` | #### `result: RxQueryResult<T>` | Property | Type | Description | | ------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------- | | `result` | `T[] \| RxDocument<T>[]` | the resulting array of objects or `RxDocument` instances, depending on `json` option | | `isFetching` | `boolean` | fetching state indicator | | `currentPage` | `number` | relevant in **all** pagination modes; holds number of current page | | `isExhausted` | `boolean` | relevant in **Infinite** pagination; flags result list as "exhausted", meaning all documents have been already fetched | | `fetchMore` | `() => void` | relevant in **Infinite** pagination; a function to be called by the consumer to request documents of the next page | | `resetList` | `() => void` | relevant in **Infinite** pagination; a function to be called by the consumer to reset paginated results | | `pageCount` | `number` | relevant in **Traditional** pagination; holds the total number of pages available | | `fetchPage` | `(page: number) => void` | relevant in **Traditional** pagination; a function to be called by the consumer to request results of a specific page | #### Simple Example ```javascript const collection = useRxCollection('characters'); const query = collection.find().where('affiliation').equals('Jedi'); const { result } = useRxQuery(query); ``` #### Infinite Scroll Pagination Example ```javascript const collection = useRxCollection('characters'); const query = collection.find().where('affiliation').equals('Jedi'); const { result: characters, isFetching, fetchMore, isExhausted, } = useRxQuery(query, { pageSize: 5, pagination: 'Infinite', }); if (isFetching) { return 'Loading...'; } return ( <CharacterList> {characters.map((character, index) => ( <Character character={character} key={index} /> ))} {!isExhausted && <button onClick={fetchMore}>load more</button>} </CharacterList> ); ``` #### Traditional Pagination Example ```javascript const collection = useRxCollection('characters'); const query = collection.find({ selector: { affiliation: 'Jedi', }, }); const { result: characters, isFetching, fetchPage, pageCount, } = useRxQuery(query, { pageSize: 5, pagination: 'Traditional', }); if (isFetching) { return 'Loading...'; } // render results and leverage pageCount to render page navigation return ( <div> <CharacterList> {characters.map((character, index) => ( <Character character={character} key={index} /> ))} </CharacterList> <div> {Array(pageCount) .fill() .map((x, i) => ( <button onClick={() => { fetchPage(i + 1); }} > page {i + 1} </button> ))} </div> </div> ); ``` <hr /> ### `useRxData` Convenience wrapper around `useRxQuery` that expects a collection name & a query constructor function ```javascript function useRxData<T>( collectionName: string, queryConstructor: ((collection: RxCollection<T>) => RxQuery<T> | undefined) | undefined, options?: UseRxQueryOptions ): RxQueryResult<T> ``` #### Example ```javascript const { result } = useRxData('characters', collection => collection.find().where('affiliation').equals('Jedi') ); ``` <hr /> ## Recipes ### Query and Query Constructor memoization By design, `useRxQuery` will re-subscribe to `query` object whenever it changes, allowing for query criteria to be modified during component updates. For this reason, to avoid unnecessary re-subscriptions, query should be memoized (i.e. via react's `useMemo`): ```javascript const { affiliation } = props; const collection = useRxCollection('characters'); const query = useMemo( () => collection.find({ selector: { affiliation, }, }), [collection, affiliation] ); const { result } = useRxQuery(query); ``` Same goes for `useRxData` and the `queryConstructor` function: ```javascript const { affiliation } = props; const queryConstructor = useCallback( collection => collection.find({ selector: { affiliation, }, }), [affiliation] ); const { result } = useRxData('characters', queryConstructor); ``` ### Lazy instantiation of RxDatabase & RxCollections All rxdb-hooks give you the ability to lazily instantiate the database and the collections within it. Initial delay until the above become available is absorbed by indicating the state as fetching (`isFetching:true`). Since `v5.0.0` of `rxdb-hooks`, observing newly created collections has become an **opt-in** feature that, _if needed_, has to be enabled via the provided `observeNewCollections` plugin: ```javascript import { addRxPlugin } from 'rxdb'; import { observeNewCollections } from 'rxdb-hooks'; addRxPlugin(observeNewCollections); ``` Adding the plugin makes it possible for all rxdb-hooks to pick up data from collections that are lazily added after the inital db initialization. Also note that lazily instantiating the rxdb instance itself is supported out-of-the-box, **the plugin only affects lazy collection creation**. ### Mutations Performing mutations on data is possible through the APIs provided by [RxDocument](https://rxdb.info/rx-document.html#functions) and [RxCollection](https://rxdb.info/rx-collection.html#functions): #### Example ```javascript const collection = useRxCollection('characters'); collection.upsert({ name: 'Luke Skywalker', affiliation: 'Jedi', }); ``` ## LICENSE MIT