v0.11 Queries, Queryable e useQuery
Além dos benefícios de desempenho e integridade de dados de normalizar o estado, ganhamos a vantagem adicional de poder acessar e consultar a store diretamente com segurança.
Nesta versão, ajustamos e simplificamos essa funcionalidade unificando tudo em torno do conceito de Schemas Queryable. Eles incluem Entity, All, Collection, Query e Union
O maior impacto dessa mudança é a introdução de um novo hook, useQuery(), que permite consultas diretas à store usando os Schemas Queryable.
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' });
De forma semelhante, podemos consultar Queryables com o controller e o snapshot usando controller.get
const bob = snapshot.get(User, { username: 'bob' });
Além disso, investimos em mais melhorias de desempenho, resultando em um ganho de cerca de 2x na maioria das operações e em Queries 16x mais rápidas.
Mudanças incompatíveis:
- useCache(new Index(MyEntity)) -> useQuery(MyEntity)
- new Query -> new schema.Query
- useCache(myQuery) -> useQuery(myQuery)
- new AbortOptimistic() -> snapshot.abort
- Entity.useIncoming → Entity.shouldUpdate
- useCache() aceita Endpoints com sideEffects (como Resource.update)
- Permite que Entity.pk() retorne números.
- Melhorias de desempenho de 2 a 16x
Querable
Schemas Queryable exigem um método queryKey() que retorne algo. Eles incluem
Entity, All, Collection, Query
e Union.
interface Queryable {
queryKey(
args: readonly any[],
queryKey: (...args: any) => any,
getEntity: GetEntity,
getIndex: GetIndex,
// `{}` means non-void
): {};
}
schema.Query
Query antes era implementado como um Endpoint. Como agora temos formas de recuperar schemas Queryable
diretamente, podemos simplesmente tornar Query mais um schema Queryable. Ele deve funcionar basicamente da mesma forma,
permitindo computações arbitrárias sobre o valor vindo de uma 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 agora não permite schemas que não sejam Queryable.
const getBob = new schema.Query( // Array is not Queryable new schema.Array({ bob: '' }), thing => { return thing; }, );
controller.get / snapshot.get
Agora podemos acessar a store apenas com um Schema Queryable - não precisamos mais de um Endpoint inteiro. #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,
};
}
Como não precisamos mais acessar outros membros do Resource para obter Post,
podemos usar a sobrecarga muito mais simples de 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
Renderiza qualquer Schema Queryable a partir da store. Queries são uma ótima companhia para renderizar com eficiência computações agregadas, como as que usam groupBy, map, reduce e 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' });
Desempenho
Novas otimizações tanto no sistema de cache quanto em todos os caminhos críticos do código deram um belo ganho de 2x às operações mais comuns. Com a adição da capacidade de cache de queries, vemos uma melhoria ainda mais impressionante de 16x em nosso benchmark de ordenação com Query.
Isso se soma à vantagem já existente (de até 23x) sobre a biblioteca normalizr legada.
Outras melhorias
- useCache() aceita Endpoints com sideEffects (como Resource.update) #2963
const lastCreated = useCache(MyResource.getList.push);
- Permite que pk retorne números #2961
Beforeclass MyEntity extends Entity {id = 0;pk() {return `${this.id}`;}}Afterclass MyEntity extends Entity {id = 0;pk() {return this.id;}}
- Tipagem
- Melhora a tipagem de .extend() ao usar verificações de null não estritas e nenhum parâmetro body #2962
Parameters<typeof new RestEndpoint({ path: '/test' }).extend({ path: '/test2' })> === []
- O tipo padrão de Args de Collection agora é: 8377e
| []| [urlParams: Record<string, any>]| [urlParams: Record<string, any>, body: any]
- Melhora a tipagem de .extend() ao usar verificações de null não estritas e nenhum parâmetro body #2962
- Correções
- O caso INVALID na desnormalização de schema.All também deve funcionar quando os nomes de classe são minificados em builds de produção #2954
unvisit()sempre retornaundefinedquando recebeundefinedcomo entrada.Allretorna INVALID dequeryKey()para invalidar o que antes era um caso especial emunvisit()(quando não há entrada na tabela para a entidade em questão)
- Entidades aninhadas ausentes devem aparecer assim que estiverem presentes #2956, #2961
- O caso INVALID na desnormalização de schema.All também deve funcionar quando os nomes de classe são minificados em builds de produção #2954
Guia de migração
Esta atualização exige atualizar todas as versões dos pacotes ao mesmo tempo.
- 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 removido
Basta usar Entities diretamente em useQuery
const UserIndex = new Index(User);
const bob = useCache(UserIndex, { username: 'bob' });
const bob = useQuery(User, { username: 'bob' });
new Query -> new schema.Query
Query agora é um schema; usado com 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 elimina a necessidade de importar 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
Torna o nome de Entity.shouldUpdate consistente com 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);
}
}
Tratamento de foco do useDLE() no React Native
Assim como useSuspense() e useFetch(), useDLE() agora pode revalidar os dados em eventos de foco.
Ao usar React Navigation, useDLE() disparará buscas no foco se os dados forem considerados desatualizados.
Isso pode resultar em mais buscas do que ocorriam anteriormente.
Mudanças potencialmente, mas improvavelmente, incompatíveis
Schema.infer() -> Schema.queryKey() #2977
A maioria das pessoas não sobrescreveu o método infer(). Se esse for o seu caso, você precisa renomeá-lo para queryKey()
e também atualizar a lista de parâmetros.
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?)
Obtém todas as entidades de um tipo com um argumento, ou uma única entidade com dois
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)
Retorna a entrada do índice (mapa de valor->pk)
const value = args[0][indexName];
return getIndex(schema.key, indexName, value)[value];
Sempre normalizar pk para o tipo string #2961
Mudança sutil no que ficará na store. Isso provavelmente só importa ao usar @data-client/normalizr diretamente.
{
"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
Isso provavelmente só importa ao consumir @data-client/core diretamente, já que a estrutura do estado interno é opaca para @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
Esses métodos são exportados em @data-client/core e @data-client/normalizr.
denormalizeCached() -> memo.denormalize() #2978
Exportado em @data-client/normalizr
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
Exportado em @data-client/normalizr
Generalizamos esse tipo de dado para que ele possa ser usado com outras dependências.
new WeakEntityMap();
new WeakDependencyMap<EntityPath>();
Suporte à atualização
Como sempre, se você tiver qualquer problema ou dúvida, fique à vontade para entrar no nosso ou registrar um bug
