跳到主要内容

React 集成

@aiao/rxdb-react 提供了 React 集成,包括 Context Provider 和一系列响应式 Hooks,让你在 React 应用中轻松使用 RxDB。

安装

npm install @aiao/rxdb @aiao/rxdb-react

核心概念

RxDBProvider

使用 RxDBProvider 将 RxDB 实例注入到 React 组件树中:

import { RxDBProvider } from '@aiao/rxdb-react';
import { RxDB } from '@aiao/rxdb';

// 创建 RxDB 实例
const rxdb = new RxDB({
dbName: 'myapp',
entities: [Todo],
sync: { type: SyncType.None, local: { adapter: 'sqlite' } }
});

function App() {
return (
<RxDBProvider db={rxdb}>
<TodoList />
</RxDBProvider>
);
}

自定义 Provider

如果需要更强的类型安全性,可以创建自定义的 Provider:

import { makeRxDBProvider } from '@aiao/rxdb-react';
import { RxDB } from '@aiao/rxdb';

// 创建类型安全的 Provider 和 Hook
const { RxDBProvider, userRxDB } = makeRxDBProvider<RxDB>();

export { RxDBProvider, userRxDB };

Hooks API

@aiao/rxdb-react 提供了一系列响应式 Hooks,自动管理订阅生命周期。

RxDBResource 接口

所有 Hooks 返回一个 RxDBResource 对象,包含以下属性:

interface RxDBResource<T> {
value: T; // 查询结果值
error: Error | undefined; // 错误信息
isLoading: boolean; // 加载状态
isEmpty: boolean | undefined; // 是否为空
hasValue: boolean; // 是否有值
}

基础查询 Hooks

useGet

根据 ID 获取单个实体:

import { useGet } from '@aiao/rxdb-react';

function TodoDetail({ id }: { id: string }) {
const { value: todo, isLoading, error } = useGet(Todo, { id });

if (isLoading) return <div>加载中...</div>;
if (error) return <div>错误: {error.message}</div>;
if (!todo) return <div>未找到</div>;

return <div>{todo.title}</div>;
}

useFindOne

查找符合条件的第一个实体:

import { useFindOne } from '@aiao/rxdb-react';

function LatestTodo() {
const { value: todo, isLoading } = useFindOne(Todo, {
where: { completed: false },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
});

if (isLoading) return <div>加载中...</div>;
return <div>{todo?.title || '没有未完成的待办'}</div>;
}

useFindOneOrFail

类似 useFindOne,但找不到时会抛出错误:

import { useFindOneOrFail } from '@aiao/rxdb-react';

function RequiredTodo({ filter }: { filter: any }) {
const { value: todo, error } = useFindOneOrFail(Todo, { where: filter });

if (error) return <div>未找到匹配的待办事项</div>;
return <div>{todo?.title}</div>;
}

useFind

查找多个符合条件的实体:

import { useFind } from '@aiao/rxdb-react';

function TodoList() {
const { value: todos, isLoading, isEmpty } = useFind(Todo, {
where: {
combinator: 'and',
rules: [{ field: 'completed', operator: '=', value: false }]
},
orderBy: [{ field: 'createdAt', sort: 'desc' }],
limit: 20
});

if (isLoading) return <div>加载中...</div>;
if (isEmpty) return <div>暂无待办事项</div>;

return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}

useFindAll

查找所有实体:

import { useFindAll } from '@aiao/rxdb-react';

function AllTodos() {
const { value: todos, isLoading } = useFindAll(Todo, {
orderBy: [{ field: 'createdAt', sort: 'desc' }]
});

if (isLoading) return <div>加载中...</div>;

return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}

useCount

统计符合条件的实体数量:

import { useCount } from '@aiao/rxdb-react';

function TodoStats() {
const { value: totalCount } = useCount(Todo, {});
const { value: completedCount } = useCount(Todo, {
where: { completed: true }
});
const { value: pendingCount } = useCount(Todo, {
where: { completed: false }
});

return (
<div>
<p>总计: {totalCount}</p>
<p>已完成: {completedCount}</p>
<p>未完成: {pendingCount}</p>
</div>
);
}

树结构 Hooks

用于查询树形结构的实体(使用 @TreeEntity 定义)。

useFindDescendants

查找所有后代节点:

import { useFindDescendants } from '@aiao/rxdb-react';

function MenuTree({ rootId }: { rootId: string }) {
const { value: descendants, isLoading } = useFindDescendants(Menu, {
id: rootId,
depth: 3 // 查询深度
});

if (isLoading) return <div>加载中...</div>;

return (
<ul>
{descendants.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}

useCountDescendants

统计后代节点数量:

import { useCountDescendants } from '@aiao/rxdb-react';

function MenuItemCount({ id }: { id: string }) {
const { value: count } = useCountDescendants(Menu, { id });
return <span>子项数量: {count}</span>;
}

useFindAncestors

查找所有祖先节点:

import { useFindAncestors } from '@aiao/rxdb-react';

function Breadcrumb({ currentId }: { currentId: string }) {
const { value: ancestors } = useFindAncestors(Menu, { id: currentId });

return (
<nav>
{ancestors.map((item, index) => (
<span key={item.id}>
{index > 0 && ' > '}
{item.name}
</span>
))}
</nav>
);
}

useCountAncestors

统计祖先节点数量:

import { useCountAncestors } from '@aiao/rxdb-react';

function MenuLevel({ id }: { id: string }) {
const { value: level } = useCountAncestors(Menu, { id });
return <span>层级: {level}</span>;
}

动态选项

所有 Hooks 都支持函数式选项,可以动态计算查询参数:

function DynamicTodoList({ filter }: { filter: string }) {
const { value: todos } = useFind(Todo, () => ({
where: {
combinator: 'and',
rules: [
{ field: 'title', operator: 'like', value: `%${filter}%` }
]
}
}));

return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}

filter 变化时,Hook 会自动重新订阅并更新数据。

响应式更新

所有 Hooks 都基于 RxJS 实现响应式更新。当数据库中的数据变化时,组件会自动重新渲染:

function LiveTodoList() {
// 数据变化时自动更新
const { value: todos } = useFind(Todo, {
where: { completed: false }
});

const handleComplete = async (todo: Todo) => {
todo.completed = true;
await todo.save();
// 列表会自动更新
};

return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.title}
<button onClick={() => handleComplete(todo)}>完成</button>
</li>
))}
</ul>
);
}

完整示例

import React, { useState } from 'react';
import { RxDBProvider, useFind, useCount } from '@aiao/rxdb-react';
import { RxDB, SyncType } from '@aiao/rxdb';
import { Todo } from './entities/Todo';

// 初始化数据库
const rxdb = new RxDB({
dbName: 'todo-app',
entities: [Todo],
sync: { type: SyncType.None, local: { adapter: 'sqlite' } }
});

function TodoApp() {
const [filter, setFilter] = useState('all');

const { value: todos, isLoading } = useFind(Todo, () => ({
where: filter === 'all'
? undefined
: { completed: filter === 'completed' },
orderBy: [{ field: 'createdAt', sort: 'desc' }]
}));

const { value: totalCount } = useCount(Todo, {});
const { value: activeCount } = useCount(Todo, {
where: { completed: false }
});

const handleAdd = async (title: string) => {
const todo = new Todo();
todo.title = title;
await todo.save();
};

const handleToggle = async (todo: Todo) => {
todo.completed = !todo.completed;
await todo.save();
};

const handleDelete = async (todo: Todo) => {
await todo.remove();
};

if (isLoading) return <div>加载中...</div>;

return (
<div>
<h1>待办事项 ({activeCount}/{totalCount})</h1>

<div>
<button onClick={() => setFilter('all')}>全部</button>
<button onClick={() => setFilter('active')}>未完成</button>
<button onClick={() => setFilter('completed')}>已完成</button>
</div>

<ul>
{todos.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo)}
/>
<span style={{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.title}
</span>
<button onClick={() => handleDelete(todo)}>删除</button>
</li>
))}
</ul>
</div>
);
}

function App() {
return (
<RxDBProvider db={rxdb}>
<TodoApp />
</RxDBProvider>
);
}

export default App;

类型安全

所有 Hooks 都是完全类型安全的,TypeScript 会自动推断实体类型和查询选项:

// TypeScript 会自动推断 todo 的类型为 Todo | undefined
const { value: todo } = useGet(Todo, { id: '123' });

// TypeScript 会验证查询选项的有效性
const { value: todos } = useFind(Todo, {
where: {
combinator: 'and',
rules: [
{ field: 'title', operator: 'like', value: '%test%' },
{ field: 'completed', operator: '=', value: true }
]
},
// TypeScript 会提示可用的字段和操作符
orderBy: [{ field: 'createdAt', sort: 'desc' }]
});

性能优化

避免不必要的重新订阅

使用 useMemouseCallback 来缓存查询选项:

import { useMemo } from 'react';

function OptimizedTodoList({ filter }: { filter: string }) {
const options = useMemo(() => ({
where: {
combinator: 'and',
rules: [{ field: 'title', operator: 'like', value: `%${filter}%` }]
}
}), [filter]);

const { value: todos } = useFind(Todo, options);

return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}

分页加载

对于大量数据,使用分页查询:

function PaginatedTodoList() {
const [page, setPage] = useState(0);
const pageSize = 20;

const { value: todos } = useFind(Todo, {
limit: pageSize,
offset: page * pageSize,
orderBy: [{ field: 'createdAt', sort: 'desc' }]
});

return (
<div>
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button onClick={() => setPage(p => Math.max(0, p - 1))}>上一页</button>
<button onClick={() => setPage(p => p + 1)}>下一页</button>
</div>
);
}

注意事项

  1. 生命周期管理:Hooks 会自动管理订阅的生命周期,在组件卸载时自动取消订阅
  2. 错误处理:始终检查 error 属性来处理查询错误
  3. 加载状态:使用 isLoading 来显示加载状态,提供更好的用户体验
  4. 空状态:使用 isEmpty 来判断查询结果是否为空
  5. 性能考虑:避免在 render 函数中创建新的选项对象,使用 useMemo 或函数式选项

参考