# rxdb-hooks

#### React hooks for integrating with [RxDB](https://github.com/pubkey/rxdb)

> [!NOTE]
> Since [`17.0.0-beta.7`](https://rxdb.info/releases/17.0.0.html), RxDB ships its own official React hooks plugin — see the [RxDB React integration guide](https://rxdb.info/react.html). If you're starting a new project on RxDB ≥ 17, the official plugin is the recommended choice. `rxdb-hooks` continues to be maintained for projects on the RxDB versions listed in the [compatibility table](#compatibility-with-rxdb) below.

<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  |
| ------------------ | ---------------------- |
| `6.x`              | `14.x`, `15.x`, `16.x` |
| `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_

> **Note:** `rxdb-hooks@6` requires TypeScript ≥ 5 (consumer-side), in line with the RxDB 15+ toolchain requirements. If you are still on TypeScript 4.x, stay on `rxdb-hooks@5`.

## 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
