Entity 与数据规范化
Entity 拥有主键,因此可以通过查找表轻松访问。这样一来,无论同一份数据出现在哪个 endpoint 中,你都能方便地查找、更新、创建或删除它。
- State
- Response
- Endpoint
- Entity
- Component

[
{ "id": 1, "title": "this is an entity" },
{ "id": 2, "title": "this is the second entity" }
]
const getPresentations = new Endpoint(
() => fetch(`/presentations`).then(res => res.json()),
{ schema: new Collection([Presentation]) },
);
class Presentation extends Entity {
id = '';
title = '';
static key = 'Presentation';
}
import { useSuspense } from '@data-client/react';
import { getPresentations } from './api/Presentation';
export function PresentationsPage() {
const presentation = useSuspense(getPresentations);
return presentation.map(presentation => (
<div key={presentation.pk()}>{presentation.title}</div>
));
}
从响应中提取 entity 的过程称为 normalization(规范化)。访问响应时则通过 denormalization(反规范化)逆转这一过程。
使用 entity 可以将 Reactive Data Client 的全局引用相等保证扩展到比整个 endpoint 响应更细的粒度。
变更与动态数据
当 endpoint 修改数据时,这称为副作用。用 sideEffect: true 标记 endpoint 会告诉 Reactive Data Client 该 endpoint 不是幂等的,因此不应在可能任意多次调用该 endpoint 的 hook 中使用它,例如 useSuspense() 或 useFetch()
只要在 endpoint 的响应中包含被修改的数据,并指定 schema,Reactive Data Client 就能更新它从中提取的所有 entity。
- Create
- Update
- Delete
import { RestEndpoint, schema } from '@data-client/rest';
const todoCreate = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos',
method: 'POST',
schema: new Collection([Todo]).push,
});
使用示例
import { useController } from '@data-client/react';
import { todoCreate } from './api/Todo';
import Form from './Form';
import FormField from './FormField';
export default function NewTodoForm() {
const ctrl = useController();
return (
<Form
onSubmit={e => ctrl.fetch(todoCreate, new FormData(e.target))}
>
<FormField name="title" />
</Form>
);
}
import { RestEndpoint } from '@data-client/rest';
const todoUpdate = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
method: 'PUT',
schema: Todo,
});
使用示例
import { useController, useSuspense } from '@data-client/react';
import { todoDetail, todoUpdate } from './api/Todo';
import Form from './Form';
import FormField from './FormField';
export default function UpdateTodoForm({ id }: { id: number }) {
const todo = useSuspense(todoDetail, { id });
const ctrl = useController();
return (
<Form
onSubmit={e =>
ctrl.fetch(todoUpdate, { id }, new FormData(e.target))
}
initialValues={todo}
>
<FormField name="title" />
</Form>
);
}
import { Invalidate, RestEndpoint } from '@data-client/rest';
const todoDelete = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos/:id',
method: 'DELETE',
schema: new Invalidate(Todo),
});
使用示例
import { useController } from '@data-client/react';
import { todoDelete, type Todo } from './api/Todo';
export default function TodoWithDelete({ todo }: { todo: Todo }) {
const ctrl = useController();
return (
<div>
{todo.title}
<button onClick={() => ctrl.fetch(todoDelete, { id: todo.id })}>
Delete
</button>
</div>
);
}
变更会自动更新规范化缓存,从而保证数据一致且新鲜。
Schema
schema 以声明式的方式定义如何处理响应
import { RestEndpoint, Collection } from '@data-client/rest';
const getTodoList = new RestEndpoint({
urlPrefix: 'https://jsonplaceholder.typicode.com',
path: '/todos',
schema: new Collection([Todo]),
});
将 Entity Todo 放入数组 Collection 中,就可以轻松地向其中 push 或 unshift 新的 Todos。
除了数组之外,还提供了另外几种适用于不同模式的 schema。前两种(Object 和 Array)可以简写为对象字面量和数组字面量。
| 数据类型 | 可变 | Schema | 描述 | 可查询 |
|---|---|---|---|---|
| 对象 | ✅ | Entity | 单个唯一对象 | ✅ |
| ✅ | Union(Entity) | 多态对象(A | B) | ✅ | |
| 🛑 | Object | 静态已知的键 | 🛑 | |
| Invalidate(Entity) | 删除 Entity | 🛑 | ||
| 列表 | ✅ | Collection(Array) | 可增长的列表 | ✅ |
| 🛑 | Array | 不可变列表 | 🛑 | |
| All | 某一类型的全部 Entity 列表 | ✅ | ||
| 映射 | ✅ | Collection(Values) | 可增长的映射 | ✅ |
| 🛑 | Values | 不可变映射 | 🛑 | |
| Scalar | ✅ | Scalar | 依赖视角(lens)的 Entity 字段 | ✅ |
| 任意 | Query(Queryable) | 记忆化的自定义转换 | ✅ | |
| Lazy(Schema) | 延迟反规范化 | ✅ |
嵌套
此外,Entity 本身也可以通过声明 static schema 成员来指定嵌套 schema。
- Entity
- Response
import { Entity } from '@data-client/endpoint';
class Todo extends Entity {
id = 0;
user = User.fromJS();
title = '';
completed = false;
static key = 'Todo';
static schema = {
user: User,
};
}
class User extends Entity {
id = 0;
username = '';
static key = 'User';
}
{
"id": 5,
"user": {
"id": 10,
"username": "bob"
},
"title": "Write some Entities",
"completed": false
}
数据表示
此外,函数也可以用作 schema,它会在反规范化期间被调用。这对于 bignumber 或 temporal instant 这类表示形式可能很有用
import { Entity } from '@data-client/endpoint';
class Todo extends Entity {
id = 0;
user = User.fromJS();
title = '';
completed = false;
dueDate = Temporal.Instant.fromEpochMilliseconds(0);
static key = 'Todo';
static schema = {
user: User,
dueDate: Temporal.Instant.from,
};
}
得益于全局引用相等保证,每次更新时成员只会构造一次。
检查 store(调试)
可以安装 DevTools 浏览器扩展来检查和调试 store。

基准测试
与非规范化方案相比,entity 级别的记忆化可带来高达 20 倍的反规范化性能提升,以及快 90 倍的变更传播速度。完整的规范化基准测试结果以及完整的 React 集成基准测试,请参阅性能页面。