Pular para o conteúdo principal

v0.11 Queries, Queryable e useQuery

· 12 min para ler
Nathaniel Tucker
Creator of Reactive Data Client

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.

Guia de migração

Mudanças incompatíveis:

Outros destaques:

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

Before
getOptimisticResponse(snapshot, { id }) {
const { data: post } = snapshot.getResponse(Base.get, { id });
if (!post) throw snapshot.abort;
return {
id,
votes: post.votes + 1,
};
}
After
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().

Before
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,
};
},
}),
}));
After
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.

Benchmarks ao longo do tempo | Ver benchmark

Outras melhorias​

  • useCache() aceita Endpoints com sideEffects (como Resource.update) #2963
    const lastCreated = useCache(MyResource.getList.push);
  • Permite que pk retorne números #2961
    Before
    class MyEntity extends Entity {
    id = 0;
    pk() {
    return `${this.id}`;
    }
    }
    After
    class 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]
  • 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 retorna undefined quando recebe undefined como entrada.
      • All retorna INVALID de queryKey() para invalidar o que antes era um caso especial em unvisit() (quando não há entrada na tabela para a entidade em questão)
    • Entidades aninhadas ausentes devem aparecer assim que estiverem presentes #2956, #2961

Guia de migração​

Esta atualização exige atualizar todas as versões dos pacotes ao mesmo tempo.

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

Index removido​

Basta usar Entities diretamente em useQuery

Before
const UserIndex = new Index(User);

const bob = useCache(UserIndex, { username: 'bob' });
After
const bob = useQuery(User, { username: 'bob' });

new Query -> new schema.Query​

Query agora é um schema; usado com useQuery

Before
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 });
After
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

Before
getOptimisticResponse(snapshot, { id }) {
const { data } = snapshot.getResponse(Base.get, { id });
if (!data) throw new AbortOptimistic();
return {
id,
votes: data.votes + 1,
};
}
After
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

Before
class MyEntity extends Entity {
static useIncoming(
existingMeta: { date: number },
incomingMeta: { date: number },
existing: any,
incoming: any,
) {
return !deepEquals(existing, incoming);
}
}
After
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.

Before
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);
}
}
After
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

One argument
const entitiesEntry = getEntity(this.schema.key);
if (entitiesEntry === undefined) return INVALID;
return Object.values(entitiesEntry).map(
entity => entity && this.schema.pk(entity),
);
Two arguments
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.

Before
{
"Article": {
"123": {
"author": 8472,
"id": 123,
"title": "A Great Article"
}
},
"User": {
"8472": {
"id": 8472,
"name": "Paul"
}
}
}
After
{
"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.

Before
{
"entities": {
"Article": {
"5": {
"content": "more things here",
"id": 5,
"title": "hi"
}
}
},
"results": {
"GET http://test.com/articles/5": "5"
}
}
After
{
"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

Before
const endpointCache = new WeakEntityMap();
const entityCache = {};
denormalizeCached(
input,
schema,
state.entities,
entityCache,
endpointCache,
args,
);
After
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.

Before
new WeakEntityMap();
After
new WeakDependencyMap<EntityPath>();

Suporte à atualização​

Como sempre, se você tiver qualquer problema ou dúvida, fique à vontade para entrar no nosso Chat ou registrar um bug