v0.11 Queries, Queryable y useQuery
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.
Cambios incompatibles:
- useCache(new Index(MyEntity)) -> useQuery(MyEntity)
- new Query -> new schema.Query
- useCache(myQuery) -> useQuery(myQuery)
- new AbortOptimistic() -> snapshot.abort
- Entity.useIncoming → Entity.shouldUpdate
- useCache() acepta Endpoints con sideEffects (como Resource.update)
- Se permite que Entity.pk() devuelva números.
- Mejoras de rendimiento de 2-16x
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
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 ya no necesitamos acceder a otros miembros del Resource para obtener Post,
podemos usar la sobrecarga mucho más simple 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 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.
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
Beforeclass MyEntity extends Entity {id = 0;pk() {return `${this.id}`;}}Afterclass 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]
- Mejora del tipado de .extend() al usar comprobaciones de null laxas y sin parámetro body #2962
- 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 devuelveundefinedcuando recibeundefinedcomo entrada.Alldevuelve INVALID desdequeryKey()para invalidar lo que antes era un caso especial enunvisit()(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
- 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
Guía de migración
Esta actualización requiere actualizar todas las versiones de los paquetes simultáneamente.
- 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 eliminado
Simplemente usa las Entities directamente en useQuery
const UserIndex = new Index(User);
const bob = useCache(UserIndex, { username: 'bob' });
const bob = useQuery(User, { username: 'bob' });
new Query -> new schema.Query
Query ahora es un schema; se usa con 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 la necesidad 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
Hace que el nombre de Entity.shouldUpdate sea consistente con 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);
}
}
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.
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?)
Obtiene todas las entidades de un tipo con un argumento, o una sola entidad con dos
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)
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.
{
"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
Probablemente solo importe al consumir @data-client/core directamente, ya que la estructura del estado interno es 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
Estos métodos se exportan en @data-client/core y @data-client/normalizr.
denormalizeCached() -> memo.denormalize() #2978
Exportado en @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 en @data-client/normalizr
Generalizamos este tipo de datos para que pueda usarse con otras dependencias.
new WeakEntityMap();
new WeakDependencyMap<EntityPath>();
Soporte para la actualización
Como siempre, si tienes algún problema o pregunta, no dudes en unirte a nuestro o reportar un error
