Saltar al contenido principal

v0.11 Queries, Queryable y useQuery

· 12 min de lectura
Nathaniel Tucker
Creator of Reactive Data Client

Además de las ventajas de rendimiento e integridad de datos de normalizar el estado, obtenemos el beneficio adicional de poder acceder al store y consultarlo directamente de forma segura.

En esta versión ajustamos y simplificamos esta funcionalidad unificándola en torno al concepto de Schemas Queryable. Estos incluyen Entity, All, Collection, Query y Union

El mayor impacto de este cambio es la introducción de un nuevo hook, useQuery(), que permite búsquedas directas en el store usando los 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 manera similar, podemos buscar Queryables con el controller y el snapshot usando controller.get

const bob = snapshot.get(User, { username: 'bob' });

Adicionalmente, hemos invertido en más mejoras de rendimiento, que resultan en un aumento de rendimiento de alrededor de 2x para la mayoría de las operaciones y en que las Queries sean 16x más rápidas.

Guía de migración

Cambios incompatibles:

Otros puntos destacados:

Querable​

Los schemas Queryable requieren un método queryKey() que devuelva algo. Estos incluyen Entity, All, Collection, Query y Union.

interface Queryable {
queryKey(
args: readonly any[],
queryKey: (...args: any) => any,
getEntity: GetEntity,
getIndex: GetIndex,
// `{}` means non-void
): {};
}

schema.Query​

Query antes se implementaba como un Endpoint. Como ahora tenemos métodos para recuperar schemas Queryable directamente, podemos simplemente convertir Query en otro schema Queryable más. Debería ser básicamente igual, permitiendo cálculos arbitrarios sobre el valor de un 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 ahora rechaza los schemas que no son Queryable.

const getBob = new schema.Query(
  // Array is not Queryable
  new schema.Array({ bob: '' }),
  thing => {
    return thing;
  },
);

controller.get / snapshot.get​

Ahora podemos acceder al store con solo un Schema Queryable; ya no necesitamos un Endpoint completo. #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 ya no necesitamos acceder a otros miembros del Resource para obtener Post, podemos usar la sobrecarga mucho más simple 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 cualquier Schema Queryable desde el store. Las Queries son un gran complemento para renderizar eficientemente cálculos agregados como los que usan groupBy, map, reduce y 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' });

Rendimiento​

Nuevas optimizaciones tanto del sistema de caché como de todas las rutas de código críticas han dado un impulso considerable de 2x a las operaciones más comunes. Con la incorporación de la capacidad de caché de queries, vemos una mejora aún más impresionante de 16x en nuestro benchmark de ordenamiento de Query.

Esto se suma a la ventaja existente (de hasta 23x) sobre la biblioteca normalizr heredada.

Benchmarks a lo largo del tiempo | Ver benchmark

Otras mejoras​

  • useCache() acepta Endpoints con sideEffects (como Resource.update) #2963
    const lastCreated = useCache(MyResource.getList.push);
  • Se permite que pk devuelva 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;
    }
    }
  • Tipado
    • Mejora del tipado de .extend() al usar comprobaciones de null laxas y sin parámetro body #2962
      Parameters<typeof new RestEndpoint({ path: '/test' }).extend({ path: '/test2' })> === []
    • El tipo predeterminado de Args de Collection es: 8377e
      | []
      | [urlParams: Record<string, any>]
      | [urlParams: Record<string, any>, body: any]
  • Correcciones
    • El caso INVALID al desnormalizar schema.All también debe funcionar cuando se alteran los nombres de clase (mangling) en builds de producción #2954
      • unvisit() siempre devuelve undefined cuando recibe undefined como entrada.
      • All devuelve INVALID desde queryKey() para invalidar lo que antes era un caso especial en unvisit() (cuando no hay entrada en la tabla para la entidad dada)
    • Las entidades anidadas faltantes deben aparecer una vez que estén presentes #2956, #2961

Guía de migración​

Esta actualización requiere actualizar todas las versiones de los paquetes simultáneamente.

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 eliminado​

Simplemente usa las Entities directamente en 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 ahora es un schema; se usa con 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 la necesidad 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​

Hace que el nombre de Entity.shouldUpdate sea consistente con 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);
}
}

Manejo de foco en React Native con useDLE()​

Al igual que useSuspense() y useFetch(), useDLE() ahora podrá revalidar los datos ante eventos de foco.

Al usar React Navigation, useDLE() disparará obtenciones de datos al recibir el foco si los datos se consideran obsoletos.

Esto podría resultar en más obtenciones de datos de las que ocurrían antes.

Cambios potencialmente incompatibles, aunque poco probables​

Schema.infer() -> Schema.queryKey() #2977​

La mayoría de las personas no ha sobrescrito el método infer(). Si lo hiciste, necesitas renombrarlo a queryKey(), además de actualizar la 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?)​

Obtiene todas las entidades de un tipo con un argumento, o una sola entidad con dos

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)​

Devuelve la entrada del índice (mapa de valor->pk)

const value = args[0][indexName];
return getIndex(schema.key, indexName, value)[value];

Siempre se normaliza pk al tipo string #2961​

Cambio sutil en lo que habrá en el store. Probablemente solo importe al usar @data-client/normalizr directamente.

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​

Probablemente solo importe al consumir @data-client/core directamente, ya que la estructura del estado interno es 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​

Estos métodos se exportan en @data-client/core y @data-client/normalizr.

denormalizeCached() -> memo.denormalize() #2978​

Exportado en @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 en @data-client/normalizr

Generalizamos este tipo de datos para que pueda usarse con otras dependencias.

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

Soporte para la actualización​

Como siempre, si tienes algún problema o pregunta, no dudes en unirte a nuestro Chat o reportar un error