React 集成
@aiao/rxdb-react 提供了 React 集成,包括 Context Provider 和一系列响应式 Hooks,让你在 React 应用中轻松使用 RxDB。
安装
- npm
- Yarn
- pnpm
- Bun
npm install @aiao/rxdb @aiao/rxdb-react @aiao/rxdb-model-react
yarn add @aiao/rxdb @aiao/rxdb-react @aiao/rxdb-model-react
pnpm add @aiao/rxdb @aiao/rxdb-react @aiao/rxdb-model-react
bun add @aiao/rxdb @aiao/rxdb-react @aiao/rxdb-model-react
@aiao/rxdb-model-react 提供了一系列开箱即用的实体组件,包括 EntityForm、EntityDialog、EntityDetail、EntityTable、EntityList 等,方便快速构建 CRUD 界面。
核心概念
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, useRxDB } = makeRxDBProvider<RxDB>();
export { RxDBProvider, useRxDB };
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 LatestTodoComponent() {
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';
type TodoFilter = {
completed?: boolean;
title?: string;
};
function RequiredTodo({ filter }: { filter: TodoFilter }) {
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 TodoListComponent() {
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 TodoStatsComponent() {
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
用于查询图形结构的实体(使用 @GraphEntity 定义)。
useGraphNeighbors
查找图结构中的邻接节点:
import { useGraphNeighbors } from '@aiao/rxdb-react';
function FriendsList({ userId }: { userId: string }) {
const {
value: neighbors,
isLoading,
isEmpty
} = useGraphNeighbors(UserNode, {
entityId: userId,
level: 2, // 查询 2 跳邻居
direction: 'both' // 双向查询
});
if (isLoading) return <div>加载中...</div>;
if (isEmpty) return <div>暂无好友</div>;
return (
<ul>
{neighbors.map(neighbor => (
<li key={neighbor.node.id}>
{neighbor.node.name} (距离: {neighbor.level})
</li>
))}
</ul>
);
}
useCountNeighbors
统计邻接节点数量:
import { useCountNeighbors } from '@aiao/rxdb-react';
function FriendCount({ id }: { id: string }) {
const { value: count } = useCountNeighbors(UserNode, {
entityId: id,
level: 1,
direction: 'out'
});
return <span>好友数量: {count}</span>;
}
useGraphPaths
查找图结构中两个节点之间的路径:
import { useGraphPaths } from '@aiao/rxdb-react';
function ConnectionPaths({ fromId, toId }: { fromId: string; toId: string }) {
const {
value: paths,
isLoading,
isEmpty
} = useGraphPaths(UserNode, {
fromId,
toId,
maxDepth: 5,
direction: 'both'
});
if (isLoading) return <div>查找路径中...</div>;
if (isEmpty) return <div>没有找到连接路径</div>;
return (
<div>
<h3>找到 {paths.length} 条路径</h3>
{paths.map((path, index) => (
<div key={index} className="path">
<span>路径长度: {path.nodes.length}</span>
<span>总权重: {path.totalWeight}</span>
<div>
{path.nodes.map((node, i) => (
<span key={node.id}>
{i > 0 && ' → '}
{node.name}
</span>
))}
</div>
</div>
))}
</div>
);
}
动态选项
所有 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' }]
});
性能优化
避免不必要的重新订阅
使用 useMemo 或 useCallback 来缓存查询选项:
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>
);
}
注意事项
- 生命周期管理:Hooks 会自动管理订阅的生命周期,在组件卸载时自动取消订阅
- 错误处理:始终检查
error属性来处理查询错误 - 加载状态:使用
isLoading来显示加载状态,提供更好的用户体验 - 空状态:使用
isEmpty来判断查询结果是否为空 - 性能考虑:避免在 render 函数中创建新的选项对象,使用
useMemo或函数式选项
实体组件
@aiao/rxdb-model-react 提供了一系列实体组件,可以快速构建 CRUD 界面。
EntityDialog
可拖拽、可调整大小、可全屏的对话框组件:
import { EntityDialog } from '@aiao/rxdb-model-react';
<EntityDialog title="编辑用户" open={isOpen} onClose={() => setIsOpen(false)}>
<div>对话框内容</div>
</EntityDialog>;
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | string | — | 对话框标题 |
open | boolean | true | 是否打开 |
onClose | () => void | — | 关闭回调 |
EntityForm
实体表单组件,支持多种字段类型:
import { EntityForm } from '@aiao/rxdb-model-react';
import type { FormFieldConfig, EntityFormData } from '@aiao/rxdb-model';
const fields: FormFieldConfig[] = [
{ field: 'name', displayName: '姓名', type: 'string' },
{ field: 'age', displayName: '年龄', type: 'integer' },
{ field: 'active', displayName: '激活', type: 'boolean' },
{ field: 'type', displayName: '类型', type: 'enum', enumValues: ['A', 'B', 'C'] },
{ field: 'birthDate', displayName: '生日', type: 'date' }
];
<EntityForm
fields={fields}
data={formData}
mode="edit"
showActions={true}
onFieldChanged={event => console.log('字段变更:', event)}
onFormSubmitted={data => console.log('提交:', data)}
/>;
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fields | FormFieldConfig[] | — | 表单字段配置 |
data | EntityFormData | — | 表单数据 |
mode | 'view' | 'edit' | 'create' | 'view' | 表单模式 |
relatedEntityProvider | RelatedEntityProvider | — | 关联实体数据提供者 |
showActions | boolean | true | 是否显示操作按钮 |
onFieldChanged | (event) => void | — | 字段变更回调 |
onFormSubmitted | (data) => void | — | 表单提交回调 |
onFormCancelled | () => void | — | 表单取消回调 |
onValidationErrors | (result) => void | — | 校验失败回调 |
支持的字段类型:string、number、integer、boolean、enum、date、oneToOne、manyToOne、stringArray、numberArray、json、keyValue。
EntityDetail
实体详情组件,带有 Tab 页签,可以同时显示表单和关联表格:
import { EntityDetail } from '@aiao/rxdb-model-react';
import type { EntityMetadata } from '@aiao/rxdb';
import { useRxDB } from '@aiao/rxdb-react';
function UserDetail({ userId }: { userId: string }) {
const rxdb = useRxDB();
const [user, setUser] = useState(null);
useEffect(() => {
rxdb.user.findOne({ id: userId }).subscribe(u => setUser(u));
}, [userId]);
if (!user) return <div>加载中...</div>;
return (
<EntityDetail
metadata={user.metadata}
formFields={buildFormFields(user.metadata, 'edit')}
formData={user.toJSON()}
formMode="edit"
open={true}
onFormSubmitted={async data => {
Object.assign(user, data);
await user.save();
}}
onClose={() => history.back()}
renderRelationTab={(tab, props) => (
<EntityList
namespace={props.relatedNamespace}
name={props.relatedEntityName}
fixedQuery={props.fixedQuery}
fixedFormData={props.fixedFormData}
creationChain={props.creationChain}
/>
)}
/>
);
}
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
metadata | EntityMetadata | — | 实体元数据 |
formFields | FormFieldConfig[] | — | 表单字段配置 |
formData | EntityFormData | — | 表单数据 |
formMode | FormMode | 'edit' | 表单模式 |
relatedEntityProvider | RelatedEntityProvider | — | 关联实体数据提供者 |
fixedFormData | EntityFormData | — | 固定表单数据(只读字段) |
creationChain | string[] | [] | 创建链(防止循环创建) |
open | boolean | true | 是否打开 |
onFormSubmitted | (data) => void | — | 表单提交回调 |
onFormCancelled | () => void | — | 表单取消回调 |
onFieldChanged | (event) => void | — | 字段变更回调 |
onValidationErrors | (result) => void | — | 校验失败回调 |
onClose | () => void | — | 关闭回调 |
renderRelationTab | (tab, props) => ReactNode | — | 自定义渲染关联 Tab 内容 |
EntityTable
基于 VTable 的实体表格组件,支持单元格编辑、复制粘贴、拖拽排序:
import { EntityTable } from '@aiao/rxdb-model-react';
import type { EntityTableRecord } from '@aiao/rxdb-model';
<EntityTable
records={records}
columns={columns}
isDarkMode={false}
idField="id"
loading={false}
onCellChanged={event => {
console.log('单元格变更:', event);
}}
onRowDeleted={record => {
console.log('删除行:', record);
}}
onBatchUpdated={items => {
console.log('批量更新:', items);
}}
onRowReordered={ids => {
console.log('行排序:', ids);
}}
/>;
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
records | EntityTableRecord[] | — | 表格数据 |
columns | ListTableConstructorOptions['columns'] | — | 列配置 |
isDarkMode | boolean | false | 是否暗色模式 |
idField | string | 'id' | ID 字段名 |
nonClearableFields | ReadonlySet<string> | new Set() | 不可清空的字段 |
loading | boolean | false | 是否加载中 |
loadingMore | boolean | false | 是否加载更多 |
loadMore | () => void | — | 加载更多回调 |
cellErrorDetector | (record, field) => string | null | — | 单元格错误检测 |
cellClearable | (record, field) => boolean | — | 单元格是否可清空 |
onCellChanged | (event) => void | — | 单元格变更回调 |
onRowDeleted | (record) => void | — | 行删除回调 |
onIconClicked | (event) => void | — | 图标点击回调 |
onBatchUpdated | (items) => void | — | 批量更新回调 |
onRowReordered | (ids) => void | — | 行排序回调 |
onScrollNearBottom | () => void | — | 滚动到底部回调 |
QueryTable
带查询功能的表格组件,结合了 EntityTable 和查询构建器:
import { QueryTable } from '@aiao/rxdb-model-react';
<QueryTable
records={records}
columns={columns}
filteredCount={filteredCount}
loading={false}
loadingMore={false}
queryActive={hasQuery}
onCellChanged={handleCellChanged}
onRowDeleted={handleRowDeleted}
onBatchUpdated={handleBatchUpdated}
/>;
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
records | EntityTableRecord[] | — | 表格数据 |
columns | ColumnsDefine | — | 列配置 |
filteredCount | number | — | 过滤后的数量 |
loading | boolean | false | 是否加载中 |
loadingMore | boolean | false | 是否加载更多 |
loadMore | () => void | — | 加载更多回调 |
queryActive | boolean | false | 查询是否激活 |
onCellChanged | (event) => void | — | 单元格变更回调 |
onIconClicked | (event) => void | — | 图标点击回调 |
onRowDeleted | (record) => void | — | 行删除回调 |
onBatchUpdated | (items) => void | — | 批量更新回调 |
EntityList
实体列表组件,集成了筛选、新增、编辑、删除、无限滚动等功能:
import { EntityList } from '@aiao/rxdb-model-react';
<EntityList
namespace="app"
name="user"
fixedQuery={fixedQuery}
fixedFormData={fixedFormData}
onViewEntity={record => {
console.log('查看实体:', record);
}}
/>;
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
namespace | string | — | 实体命名空间 |
name | string | — | 实体名称 |
fixedQuery | FilterQuery | — | 固定查询条件 |
fixedFormData | EntityFormData | — | 固定表单数据(只读字段) |
creationChain | string[] | [] | 创建链(防止循环创建) |
relationKind | RelationKind | — | 关系类型 |
parentEntity | EntityInstance | null | — | 父实体实例 |
draftParentEntity | EntityInstance | null | — | 草稿父实体实例 |
parentRelationName | string | — | 父实体关系名称 |
mode | 'default' | 'select' | 'default' | 列表模式 |
alreadyLinkedIds | Set<string> | new Set() | 已关联的 ID |
onViewEntity | (record) => void | — | 查看实体回调 |
onSelectionConfirmed | (entities) => void | — | 选择确认回调(select 模式) |
onSelectionCancelled | () => void | — | 选择取消回调 |
支持功能:
- 无限滚动加载
- 筛选查询(通过
QueryBuilder) - 新增实体
- 编辑实体(单元格编辑)
- 删除实体
- 拖拽排序
- 撤销/重做(Ctrl+Z / Ctrl+Shift+Z)
- M2M 关联选择
- Undo/Redo 支持