A set of simple hooks for integrating a React application with RxDB.
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)
# using npm
npm install rxdb-hooks
# using yarn
yarn add rxdb-hooks
Root.jsx:
import React, { useEffect } from 'react';
import { Provider } from 'rxdb-hooks';
import initialize from './initialize';
const Root = () => {
const [db, setDb] = useState();
useEffect(() => {
// Notice that RxDB instantiation is asynchronous;
// until db becomes available consumer hooks that depend
// on it will still work, absorbing the delay by
// setting their state to isFetching:true
const initDB = async () => {
const _db = await initialize();
setDb(_db);
};
initDB();
}, []);
// Provide RxDB instance; hooks can now be used
// within the context of the Provider
return (
<Provider db={db}>
<App />
</Provider>
);
};
Consumer.jsx:
import React from 'react';
import { useRxData } from 'rxdb-hooks';
const Consumer = () => {
const queryConstructor = collection =>
collection
.find()
.where('affiliation')
.equals('jedi');
const { result: characters, isFetching } = useRxData(
'characters',
queryConstructor
);
if (isFetching) {
return 'loading characters...';
}
return (
<ul>
{characters.map((character, idx) => (
<li key={idx}>{character.name}</li>
))}
</ul>
);
};
initialize.js:
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',
properties: {
id: {
type: 'string',
primary: true,
},
name: {
type: 'string',
},
},
},
},
});
// maybe sync collection to a remote
// ...
return db;
};
Version 3 of rxdb-hooks breaks compatibility with rxdb 8 or lower, so you need to upgrade to rxdb 9.x. The core API is otherwise the same and should not cause any more breaking changes.
The <Provider />
makes the RxDatabase instance available to nested components and is required for all subsequent hooks to work.
Property | Type | Required | Default | Description |
---|---|---|---|---|
db |
RxDatabase |
* | - | the RxDatabase instance to consume data from |
idAttribute |
string |
- | "_id" |
used by useRxDocument when querying for single documents |
Returns the RxDatabase instance made available by the <Provider />
function useRxDB(): RxDatabase
const db = useRxDB();
Given a collection name returns an RxCollection instance, if found in RxDatabase.
function useRxCollection<T>(name: string): RxCollection<T> | null
const collection = useRxCollection('characters');
Subscribes to given RxQuery object providing query results and some helpful extra state variables.
function useRxQuery<T>(query: RxQuery, options?: UseRxQueryOptions): RxQueryResult<T>
Option | Type | Required | Default | Description |
---|---|---|---|---|
pageSize |
number |
- | - | enables pagination & defines page limit |
pagination |
"Traditional" | "Infinite" |
- | "Traditional" |
determines pagination mode; 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. Infinite: first page of results is rendered, allowing for gradually requesting more. |
json |
boolean |
- | false |
when true resulting documents will be converted to plain JavaScript objects; equivalent to manually calling .toJSON() on each RxDocument |
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 |
const collection = useRxCollection('characters');
const query = collection
.find()
.where('affiliation')
.equals('Jedi');
const { result } = useRxQuery(query);
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>
);
const collection = useRxCollection('characters');
const query = collection
.find()
.where('affiliation')
.equals('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>
);
Convenience wrapper around useRxQuery
that expects a collection name & a query constructor function
function useRxData<T>(
collectionName: string,
queryConstructor: ((collection: RxCollection<T>) => RxQuery<T> | undefined) | undefined,
options?: UseRxQueryOptions
): RxQueryResult<T>
const { result } = useRxData('characters', collection =>
collection
.find()
.where('affiliation')
.equals('Jedi')
);
Convenience hook for fetching a single document from a collection.
function useRxDocument<T>(
collectionName: string,
id?: string | number,
options?: UseRxDocumentOptions
): RxDocumentRet<T>
The id of the document
Option | Type | Required | Default | Description |
---|---|---|---|---|
idAttribute |
string |
- | "_id" |
enables overriding the id attribute; has precedence over idAttribute set by the <Provider /> |
json |
boolean |
- | false |
converts resulting RxDocument to plain javascript object |
const { result: Yoda } = useRxDocument('characters', 'Yoda', {
idAttribute: 'name',
});
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
):
const collection = useRxCollection('characters');
const query = useMemo(
() =>
collection
.find()
.where('affiliation')
.equals(affiliation), // π could come from component props
[collection, affiliation]
);
const { result } = useRxQuery(query);
Same goes for useRxData
and the queryConstructor
function:
const queryConstructor = useCallback(
collection =>
collection
.find()
.where('affiliation')
.equals(affiliation), // π could come from component props
[affiliation]
);
const { result } = useRxData('characters', queryConstructor);
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
)
Performing mutations on data is possible through the APIs provided by RxDocument and RxCollection:
const collection = useRxCollection('characters');
collection.upsert({
name: 'Luke Skywalker',
affiliation: 'Jedi',
});
MIT