v0.11 Query、Queryable 与 useQuery
除了规范化状态带来的性能和数据完整性收益之外,我们还获得了一项额外的好处:能够安全地直接访问并查询 store。
在本次发布中,我们围绕 Queryable Schema 的概念统一并简化了这一功能。它们包括 Entity、All、Collection、Query 和 Union
这一变化最大的影响是引入了新的 hook useQuery(),它允许使用 Queryable Schema 直接查找 store。
class User extends Entity {
username = '';
id = '';
groupId = '';
pk() {
return this.id;
}
static index = ['username' as const];
}
const bob = useQuery(User, { username: 'bob' });
const bob = useQuery(User, { id: '5' });
同样地,我们可以通过 controller.get 在 controller 和 snapshot 中查找 Queryable
const bob = snapshot.get(User, { username: 'bob' });
此外,我们还投入了更多的性能改进,使大多数操作的性能提升了约 2 倍,而 Query 则快了 16 倍。
破坏性变更:
- useCache(new Index(MyEntity)) -> useQuery(MyEntity)
- new Query -> new schema.Query
- useCache(myQuery) -> useQuery(myQuery)
- new AbortOptimistic() -> snapshot.abort
- Entity.useIncoming → Entity.shouldUpdate
- useCache() 接受带有 sideEffects 的 Endpoint(例如 Resource.update)
- 允许 Entity.pk() 返回数字。
- 2 到 16 倍的性能改进
Queryable
Queryable schema 需要一个返回非空值的 queryKey() 方法。它们包括
Entity、All、Collection、Query
和 Union。
interface Queryable {
queryKey(
args: readonly any[],
queryKey: (...args: any) => any,
getEntity: GetEntity,
getIndex: GetIndex,
// `{}` means non-void
): {};
}
schema.Query
Query 之前是作为 Endpoint 实现的。既然我们现在有了直接获取 Queryable
schema 的方法,就可以干脆让 Query 本身也成为一个 Queryable schema。它的用法基本保持不变,允许对 store 中的值进行任意计算。#2921
const getUserCount = new schema.Query(
new schema.All(User),
(entries, { isAdmin } = {}) => {
if (isAdmin !== undefined)
return entries.filter(user => user.isAdmin === isAdmin).length;
return entries.length;
},
);
const userCount = useQuery(getUserCount);
const adminCount = useQuery(getUserCount, { isAdmin: true });
schema.Query 现在不再允许使用非 Queryable 的 schema。
const getBob = new schema.Query( // Array is not Queryable new schema.Array({ bob: '' }), thing => { return thing; }, );
controller.get / snapshot.get
现在只需一个 Queryable Schema 即可访问 store——不再需要一整个 Endpoint。 #2921
getOptimisticResponse(snapshot, { id }) {
const { data: post } = snapshot.getResponse(Base.get, { id });
if (!post) throw snapshot.abort;
return {
id,
votes: post.votes + 1,
};
}
getOptimisticResponse(snapshot, { id }) {
const post = snapshot.get(Post, { id });
if (!post) throw snapshot.abort;
return {
id,
votes: post.votes + 1,
};
}
由于我们不再需要访问其他 Resource 成员来获取 Post,就可以使用简单得多的 Resource.extend() 重载。
export const PostResource = createResource({
path: '/posts/:id',
schema: Post,
}).extend(Base => ({
vote: new RestEndpoint({
path: '/posts/:id/vote',
method: 'POST',
body: undefined,
schema: Post,
getOptimisticResponse(snapshot, { id }) {
const { data: post } = snapshot.getResponse(Base.get, { id });
if (!post) throw snapshot.abort;
return {
id,
votes: post.votes + 1,
};
},
}),
}));
export const PostResource = createResource({
path: '/posts/:id',
schema: Post,
}).extend('vote', {
path: '/posts/:id/vote',
method: 'POST',
body: undefined,
schema: Post,
getOptimisticResponse(snapshot, { id }) {
const post = snapshot.get(Post, { id });
if (!post) throw snapshot.abort;
return {
id,
votes: post.votes + 1,
};
},
});
useQuery
从 store 中渲染任意 Queryable Schema。 Query 是高效渲染聚合计算的绝佳搭档,例如那些使用 groupBy、 map、reduce 和 filter 的计算。#2921
class User extends Entity {
username = '';
id = '';
groupId = '';
pk() {
return this.id;
}
static index = ['username' as const];
}
const bob = useQuery(User, { username: 'bob' });
const getUserCount = new schema.Query(
new schema.All(User),
(entries, { isAdmin } = {}) => {
if (isAdmin !== undefined)
return entries.filter(user => user.isAdmin === isAdmin).length;
return entries.length;
},
);
const userCount = useQuery(getUserCount);
const adminCount = useQuery(getUserCount, { isAdmin: true });
const UserCollection = new schema.Collection([User], {
argsKey: (urlParams: { groupId?: string }) => ({
...urlParams,
}),
});
const usersInGroup = useQuery(UserCollection, { groupId: '5' });
性能
对缓存系统以及所有关键代码路径的进一步优化,为最常见的操作带来了 2 倍的可观提升。再加上新增的查询缓存能力,我们的 Query 排序基准测试更是取得了令人印象深刻的 16 倍提升。
这些都叠加在原有的(最高 23 倍)相对旧版 normalizr 库的领先优势之上。
其他改进
- useCache() 接受带有 sideEffects 的 Endpoint(例如 Resource.update)#2963
const lastCreated = useCache(MyResource.getList.push);
- 允许 pk 返回数字 #2961
Beforeclass MyEntity extends Entity {id = 0;pk() {return `${this.id}`;}}Afterclass MyEntity extends Entity {id = 0;pk() {return this.id;}}
- 类型
- 修复
迁移指南
本次升级需要同时更新所有包的版本。
- NPM
- Yarn
- pnpm
- esm.sh
npm install --save @data-client/react@^0.11.0 @data-client/rest@^0.11.0 @data-client/test@^0.11.0 @data-client/img@^0.11.0 @data-client/hooks@^0.11.0
yarn add @data-client/react@^0.11.0 @data-client/rest@^0.11.0 @data-client/test@^0.11.0 @data-client/img@^0.11.0 @data-client/hooks@^0.11.0
pnpm add @data-client/react@^0.11.0 @data-client/rest@^0.11.0 @data-client/test@^0.11.0 @data-client/img@^0.11.0 @data-client/hooks@^0.11.0
<script type="importmap">
{
"imports": {
"@data-client/react": "https://esm.sh/@data-client/react@^0.11.0",
"@data-client/rest": "https://esm.sh/@data-client/rest@^0.11.0",
"@data-client/test": "https://esm.sh/@data-client/test@^0.11.0",
"@data-client/img": "https://esm.sh/@data-client/img@^0.11.0",
"@data-client/hooks": "https://esm.sh/@data-client/hooks@^0.11.0"
}
}
</script>
移除 Index
直接在 useQuery 中使用 Entity 即可
const UserIndex = new Index(User);
const bob = useCache(UserIndex, { username: 'bob' });
const bob = useQuery(User, { username: 'bob' });
new Query -> new schema.Query
Query 现在是一个 schema;与 useQuery 搭配使用
const getUserCount = new Query(
new schema.All(User),
(entries, { isAdmin } = {}) => {
if (isAdmin !== undefined)
return entries.filter(user => user.isAdmin === isAdmin).length;
return entries.length;
},
);
const userCount = useCache(getUserCount);
const adminCount = useCache(getUserCount, { isAdmin: true });
const getUserCount = new schema.Query(
new schema.All(User),
(entries, { isAdmin } = {}) => {
if (isAdmin !== undefined)
return entries.filter(user => user.isAdmin === isAdmin).length;
return entries.length;
},
);
const userCount = useQuery(getUserCount);
const adminCount = useQuery(getUserCount, { isAdmin: true });
new AbortOptimistic() -> snapshot.abort
snapshot.abort 让你不再需要导入 AbortOptimistic #2957
getOptimisticResponse(snapshot, { id }) {
const { data } = snapshot.getResponse(Base.get, { id });
if (!data) throw new AbortOptimistic();
return {
id,
votes: data.votes + 1,
};
}
getOptimisticResponse(snapshot, { id }) {
const { data } = snapshot.getResponse(Base.get, { id });
if (!data) throw snapshot.abort;
return {
id,
votes: data.votes + 1,
};
}
Entity.useIncoming → Entity.shouldUpdate
让 Entity.shouldUpdate 的命名与 Entity.shouldReorder 保持一致 #2972
class MyEntity extends Entity {
static useIncoming(
existingMeta: { date: number },
incomingMeta: { date: number },
existing: any,
incoming: any,
) {
return !deepEquals(existing, incoming);
}
}
class MyEntity extends Entity {
static shouldUpdate(
existingMeta: { date: number },
incomingMeta: { date: number },
existing: any,
incoming: any,
) {
return !deepEquals(existing, incoming);
}
}
useDLE() 在 React Native 中的焦点处理
与 useSuspense() 和 useFetch() 一样,useDLE() 现在可能会在焦点事件发生时重新验证数据。
使用 React Navigation 时,如果数据被视为过时,useDLE() 会在获得焦点时触发获取。
这可能导致比以前更多的获取请求。
可能但不太会遇到的破坏性变更
Schema.infer() -> Schema.queryKey() #2977
大多数人都没有重写过 infer() 方法。如果你重写了,需要将其重命名为 queryKey(),并同时更新参数列表。
class MyEntity extends Entity {
static infer(
args: readonly any[],
indexes: NormalizedIndex,
recurse: any,
entities: any,
): any {
if (SILLYCONDITION) return undefined;
if (entities[this.key]?.[someId]) return someId;
return super.infer(args, indexes, recurse, entities);
}
}
class MyEntity extends Entity {
static queryKey(
args: readonly any[],
queryKey: (...args: any) => any,
getEntity: GetEntity,
getIndex: GetIndex,
): any {
if (SILLYCONDITION) return undefined;
if (getEntity(this.key, someId)) return someId;
return super.queryKey(args, queryKey, getEntity, getIndex);
}
}
getEntity(key, pk?)
传入一个参数时获取某一类型的全部实体,传入两个参数时获取单个实体
const entitiesEntry = getEntity(this.schema.key);
if (entitiesEntry === undefined) return INVALID;
return Object.values(entitiesEntry).map(
entity => entity && this.schema.pk(entity),
);
if (getEntity(this.key, id)) return id;
getIndex(key, indexName, value)
返回索引条目(value->pk 的映射)
const value = args[0][indexName];
return getIndex(schema.key, indexName, value)[value];
pk 始终规范化为字符串类型 #2961
这是对 store 中内容的一个细微变化。这很可能只在直接使用 @data-client/normalizr 时才有影响。
{
"Article": {
"123": {
"author": 8472,
"id": 123,
"title": "A Great Article"
}
},
"User": {
"8472": {
"id": 8472,
"name": "Paul"
}
}
}
{
"Article": {
"123": {
"author": "8472",
"id": 123,
"title": "A Great Article"
}
},
"User": {
"8472": {
"id": 8472,
"name": "Paul"
}
}
}
state.results -> state.endpoints #2971
这很可能只在直接使用 @data-client/core 时才有影响,因为内部状态的结构对 @data-client/react 来说是不透明的。
{
"entities": {
"Article": {
"5": {
"content": "more things here",
"id": 5,
"title": "hi"
}
}
},
"results": {
"GET http://test.com/articles/5": "5"
}
}
{
"entities": {
"Article": {
"5": {
"content": "more things here",
"id": 5,
"title": "hi"
}
}
},
"endpoints": {
"GET http://test.com/articles/5": "5"
}
}
inferResults() -> memo.buildQueryKey() #2977、#2978
这些方法由 @data-client/core 和 @data-client/normalizr 导出。
denormalizeCached() -> memo.denormalize() #2978
const endpointCache = new WeakEntityMap();
const entityCache = {};
denormalizeCached(
input,
schema,
state.entities,
entityCache,
endpointCache,
args,
);
const memo = new MemoCached();
memo.denormalize(
input,
schema,
state.entities,
args,
);
WeakEntityMap -> WeakDependencyMap #2978
我们将这一数据类型泛化,使其可以用于其他依赖。
new WeakEntityMap();
new WeakDependencyMap<EntityPath>();
升级支持
一如既往,如果你遇到任何问题或有任何疑问,欢迎加入我们的 或提交 bug
