v0.19: Controller.set() por lotes, TypeScript más rápido
El original en inglés cambió después de esta traducción, así que puede estar desactualizada.
v0.19 permite que un Manager escriba un lote completo de filas recibidas por streaming en una sola actualización del store, verifica los tipos del código de endpoints unas 2 veces más rápido y corrige una ronda de problemas de TypeScript y Vue. Los agentes de programación también reciben un plugin de Claude Code, skills de Vue y llms.txt; consulta Data Client para agentes de programación.
Nuevas APIs:
- Controller.set() con schemas Array - Escribe muchas entidades en una sola actualización del store con
ctrl.set([Entity], rows), hasta 95 veces más rápido que unset()por fila
Rendimiento:
- TypeScript más rápido - Los editores y
tscverifican el código de RestEndpoint y resource() unas 2 veces más rápido con 40% menos memoria, detectando todos los errores que detectaban antes
Otras mejoras:
- Se corrige la resolución de módulos de TypeScript 7 para los
exportsdel paquete; los imports ahora resuelven a los archivos de declaración (#4019) - renderDataHook() ejecuta los efectos de montaje del provider cuando el primer renderizado suspende (#4099)
- Los valores de Controller.set() se tipan según el schema, así que
ctrl.set(new schema.All(Todo), 42)es un error de TypeScript (#4133) - Controller.set() elimina una sola entidad con un schema Invalidate sin la solución alternativa del lote de una fila (#4230)
- process() recibe params tipados en
new RestEndpoint(),.extend()yresource().extend(), así que un parámetro de ruta incorrecto es un error de TypeScript en lugar deundefineden tiempo de ejecución (#4183, #4184) - Los valores de retorno de getOptimisticResponse() se tipan según el schema, así que un tipo de campo incorrecto es un error de TypeScript en lugar de un destello de datos erróneos (#4198)
- useSuspense() y useLive() de Vue conservan los datos anteriores mientras cargan los nuevos argumentos, en lugar de devolver
undefined(#4131) - useSuspense() y useLive() de Vue envían los errores de obtención posteriores a un cambio de argumentos a
onErrorCaptured()en lugar de producir un rechazo de promesa no manejado (#4135) - useSuspense(), useDLE() y useFetch() de Vue ya no vuelven a obtener los datos obsoletos en cada actualización del store, así que un
controller.set()no se sobrescribe (#4134) - useSuspense() de Vue muestra los datos obsoletos al montarse y vuelve a obtenerlos en segundo plano, como en React, en lugar de mostrar el fallback de
<Suspense>hasta que termina la nueva obtención (#4169) - useDLE() y useCache() de Vue conservan los datos
invalidIfStalecaducados a través de actualizaciones del store no relacionadas, en lugar de quedarse atascados cargando (#4142) DataClientPluginde Vue se instala en versiones de Vue anteriores a 3.5 en lugar de lanzarapp.onUnmount is not a function(#4146)- DataClientPlugin de Vue recolecta basura de forma predeterminada, igual que DataProvider de React, así que ya no necesitas pasar
gcPolicy; cómo decide qué eliminar (#4148) - Los composables de Vue aceptan argumentos getter como
() => ({ id: props.id }); estaban tipados para permitirlos pero pasaban la propia función al endpoint (#4115) - Se corrigen los errores
Cannot find name 'NoInfer'yexport typeen TypeScript 4.x conskipLibCheckdesactivado (#4138) - Se corrigen los errores de tipo de
Entity,Endpoint,UnionyRestEndpointen TypeScript 4.0–4.5 conskipLibCheckdesactivado (#4140) - Se corrigen los errores
',' expectedal importar@data-client/react/redux,@data-client/react/nextjso@data-client/testen TypeScript 4.0–4.4 (#4232) - Se corrige que
actyrenderHookde@data-client/testse tiparan comoanyen TypeScript 4.0–4.4 (#4251) - Las clases Entity pueden usarse donde se espera un
EntityInterface(#4149); prepara las sobrescrituras depk()para una versión futura - useCache() y useDLE() devuelven
undefinedpara una entidad eliminada cuya nueva obtención falló, en lugar de unSymbolverdadero que se colaba por las comprobacionesif (!data); elimina cualquier solución alternativa (#4150) - useFetch() de Vue evita que sus datos sean recolectados como basura mientras está montado, como useSuspense(), así que un
gcPolicyconfigurado ya no expulsa los datos precargados que los componentes leen después (#4152) - Las aplicaciones con Redux DevTools abierto ya no se entrecortan en desarrollo con stores grandes o actualizaciones frecuentes; cada actualización se serializa entre 40 y 60 veces más rápido (#4163)
- resource().extend() con un objeto de sobrescrituras de endpoints conserva en su tipo los endpoints añadidos antes con
.extend('name', options), así que usarlos ya no es un error de TypeScript (#4184) - RestEndpoint.push,
unshiftyremoveaceptan los campos de la Entity como cuerpo cuando un Query o Lazy envuelve la Collection, en lugar de soloFormData(#4226) - useFetch() de Vue se tipa como el
Refde solo lectura que devuelve, así quepromise.resolved(siempreundefined) es ahora un error de TypeScript; leepromise.value.resolveden su lugar (#4114) - Una subclase de
Controllerpasada a DataProvider o DataClientPlugin ya no necesita una conversión de tipo para verificar los tipos (#4228) - El
storede prepareStore() puede pasarse al<Provider>de react-redux sin una conversión de tipo, y tienedispatch()en su tipo (#4238) - resource().create acepta FormData sin una conversión de tipo, como
update,partialUpdateygetList.push(#4235) - RestEndpoint.push,
unshiftyremoveenvían un cuerpo de tipo array como JSON, así que crear o eliminar varios elementos a la vez ya no envía"[object Object]"a tu servidor;resource().createtambién acepta un array (#4262) createDataClient()de Vue queda obsoleto, ya que es interno de DataClientPlugin; instala el plugin conapp.use(DataClientPlugin, options)en lugar de llamarlo directamente (#4193)waitForNextUpdate()derenderDataCompose()de Vue queda obsoleto, ya que se rinde en silencio después de 1 segundo y deja pasar una prueba mientras sigue suspendida; usaawait resulten su lugar, oawait allSettled()después de un cambio, que ahora también espera las obtenciones que inicia un cambio de prop (pruebas unitarias de composables, #4221)
- Se requiere TypeScript 4.0 o posterior - se eliminan las declaraciones para TypeScript 3.x (#4151)
Prepárate para versiones futuras:
- Tipa
argscomoreadonlyen las sobrescrituras deEntity.pk() - Usa
DataClientPluginen lugar decreateDataClient()de Vue - Reemplaza
waitForNextUpdate()en las pruebas de Vue
Controller.set() por lotes
Un Manager que recibe un flujo de filas (como cotizaciones de precios por un websocket) ahora puede escribirlas
todas con un solo Controller.set() pasando un schema Array.
Antes de v0.19, [Ticker] era un error de TypeScript, así que la solución alternativa habitual era un set() por fila:
ws.onmessage = event => {
const rows = JSON.parse(event.data);
for (const row of rows) {
ctrl.set(Ticker, { product_id: row.product_id }, row);
}
};
ws.onmessage = event => {
const rows = JSON.parse(event.data);
ctrl.set([Ticker], rows);
};
Cada fila se fusiona con su entidad almacenada, y las entidades que no están en la lista quedan intactas. Los schemas Array no reciben args ni
función actualizadora. Para agrupar en un lote tipos de Entity mixtos, eliminaciones o filas identificadas por id, pasa un schema Union,
Invalidate o Values; consulta
Controller.set() para ver ejemplos.
#4103
Prueba ambos botones a continuación. Esta comprobación en el navegador parte de un store vacío y mide el tiempo de un Promise.all de 500 llamadas a set()
frente a un único set() por lotes. Ambos caminos son un solo commit de React, y cada uno escribe 500 precios nuevos.
import React from 'react'; import { useController, useQuery } from '@data-client/react'; import { Ticker, newPrices } from './Ticker'; function PriceStream() { const ctrl = useController(); const [timing, setTiming] = React.useState(''); const first = useQuery(Ticker, { product_id: 'COIN-0' }); const time = async ( label: string, write: (rows: ReturnType<typeof newPrices>) => Promise<unknown>, ) => { const rows = newPrices(); const start = performance.now(); await write(rows); setTiming(`${label}: ${(performance.now() - start).toFixed(1)} ms`); }; const perRow = () => time('500 set() calls', rows => Promise.all( rows.map(row => ctrl.set(Ticker, { product_id: row.product_id }, row)), ), ); // highlight-next-line const batch = () => time('1 batch set()', rows => ctrl.set([Ticker], rows)); return ( <div> <button onClick={perRow}>set() per row</button>{' '} <button onClick={batch}>batch set()</button> <p>COIN-0: {first ? `$${first.price}` : 'no data yet'}</p> <p>{timing}</p> </div> ); } render(<PriceStream />);
Rendimiento
Cada set() es una actualización del store independiente, y cada actualización del store copia la tabla de ese tipo de entidad. Escribir las filas una
por una repite esa copia por cada fila, así que el costo crece tanto con el tamaño del lote como con el tamaño del store. Un lote
paga esa copia una sola vez.
El gráfico es el benchmark setMany de node en
examples/benchmark/core.js:
un store que ya contiene 500 entidades, y luego un set() sincrónico por fila frente a un solo set([Ticker], rows).
Mide la actualización del store: 20 veces más rápido para 50 filas y 95 veces más rápido para 500 filas.
#4103
En un Manager, almacena en un búfer los mensajes entrantes y vacía cada lote con un solo set(), como se describe en
Agrupar actualizaciones de alta frecuencia. El StreamManager de la aplicación de criptomonedas ahora
vacía los mensajes de cotización de Coinbase de esta manera.
handleMessage(msg: any) {
if (msg.type in this.entities) {
(this.buffer[msg.type] ??= {})[msg.product_id] = msg;
this.flushTimeout ??= setTimeout(this.flush, 50);
}
}
flush = () => {
const buffer = this.buffer;
this.buffer = {};
this.flushTimeout = undefined;
for (const type in buffer) {
this.controller.set([this.entities[type]], Object.values(buffer[type]));
}
};
Explora el ejemplo coin-app
Si filtras las acciones de DevToolsManager por schema, el action.schema de una escritura por lotes
es el schema Array, así que compara con action.schema[0] para [Ticker] en lugar de con la propia Entity.
TypeScript más rápido
TypeScript vuelve a verificar tu código con cada pulsación de tecla en el editor y en cada build de CI. En aplicaciones con muchos endpoints,
los tipos pesados de las bibliotecas se manifiestan como un autocompletado lento, subrayados rojos que tardan segundos en aparecer y builds más lentos.
v0.19 hace que RestEndpoint, resource() y .extend() sean mucho más baratos
de verificar, sin cambios de código de tu parte. TypeScript sigue reportando todos los errores que reportaba antes
(#4173).
Resultados
Nuestra prueba de estrés más pesada, un archivo de 150 RestEndpoints con rutas largas, .extend() y .paginated(), ahora se verifica
2 veces más rápido en TypeScript 6 y 2.2 veces más rápido en TypeScript 7, usando alrededor de 40% menos memoria. Todas las cifras comparan
los paquetes publicados de v0.18.1 con v0.19; los tiempos de verificación son el mejor de 10 ejecuciones.
El código con muchos endpoints hace mucho menos trabajo de tipos. Las instanciaciones de tipos son la unidad de trabajo de TypeScript: son deterministas, así que se comparan limpiamente entre máquinas. Union hace más trabajo que en v0.18, porque v0.19 ahora verifica los tipos de los valores de set() (ver más abajo).
Pasa el cursor o toca una celda para ver las cifras exactas.
| TS 6 check time | TS 7 check time | TS 6 memory | |
|---|---|---|---|
| Long paths 150 RestEndpoints with 6-param paths, .extend() and .paginated() | -48% 3.83s → 1.98s | -54% 1.5s → 0.69s | -42% 365MB → 211MB |
| Typical app A few resources with .extend(), .paginated() and hooks | same 0.42s → 0.42s | -15% 0.062s → 0.053s | +3% 106MB → 109MB |
| React hooks 40 resources through every hook, plus ctrl.fetch() and ctrl.set() | -10% 1.24s → 1.12s | -7% 0.41s → 0.38s | -6% 167MB → 157MB |
| Vue The same 40 resources through every composable | same 0.68s → 0.68s | -12% 0.17s → 0.15s | -4% 145MB → 139MB |
| 300 fields One Entity with 300 fields, read and updated 100 times | -14% 0.44s → 0.38s | -40% 0.086s → 0.052s | -9% 111MB → 101MB |
| Schemas All, Query, Invalidate, Array, Object and Collection | -3% 1.1s → 1.07s | -14% 0.36s → 0.31s | -4% 156MB → 150MB |
| Union A 30-member Union in a Collection and Values | -2% 0.42s → 0.41s | -4% 0.071s → 0.068s | -6% 107MB → 101MB |
| set() values 1000 ctrl.set() calls on a 30-member Union, a Collection of it and a 300-field Entity | -28% 1.49s → 1.07s | -54% 0.54s → 0.25s | -26% 176MB → 131MB |
| set() updaters 1000 ctrl.set(Union, args, prev => ...) updaters on a 30-member Union | -4% 3.58s → 3.43s | +11% 1.67s → 1.85s | +1% 381MB → 384MB |
Las filas de set() miden los valores tipados de set(), que v0.18 no verificaba en absoluto. Los valores simples siguen verificándose más rápido que antes, y las funciones actualizadoras sobre Unions grandes se verifican aproximadamente en el mismo tiempo aunque TypeScript ahora verifica el valor de retorno de cada actualizadora (#4179).
Los archivos pequeños están dominados por el costo fijo de arranque de TypeScript (cargar lib.dom.d.ts por sí solo ocupa unos 100MB), así que
su tiempo y memoria apenas cambian. El ahorro se acumula a medida que crece una base de código.
Qué cambió
.extend()y.paginated()se comparten entre todos los endpoints en lugar de recrearse para cada tipo de endpoint.- Las opciones de los endpoints se infieren como tipos de objeto simples, así que TypeScript deja de reconstruirlas en cada uso.
- Los parámetros de ruta como
/users/:idse leen en una sola pasada.
Antes de publicar, desactivamos todos los @ts-expect-error del conjunto de pruebas en TypeScript 4.0 a 7 y confirmamos
que cada error sigue apareciendo en el mismo lugar.
En TypeScript 5.x y anteriores, un método process(value, params) pasado a .extend() tampoco falla ya con
"implicitly has an 'any' type" bajo strict.
Otras mejoras
Valores tipados de set()
Controller.set() antes aceptaba cualquier valor para un schema, así que un error tipográfico o un tipo de campo
incorrecto solo se manifestaba como datos erróneos en tiempo de ejecución. Los valores ahora se tipan según el schema: una
Entity recibe sus campos, mientras que una Collection, All o
Array recibe una lista de filas. Todos los campos son opcionales, ya que set() se fusiona con lo que ya está almacenado, y
los números y las cadenas son intercambiables, igual que en las respuestas de la API.
#4133
Pasa el cursor sobre los subrayados rojos para ver cada error.
import type { Controller } from '@data-client/react'; import { schema } from '@data-client/rest'; import { Todo, TodoResource } from './Todo'; export function updateTodos(ctrl: Controller) { // ✅ rows are partial Todos; ids may be strings or numbers ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]); ctrl.set(Todo, { id: 5 }, todo => ({ completed: !todo.completed })); ctrl.set([Todo], [{ id: 1, title: 'first' }, { id: 2 }]); // ❌ All takes a list of rows ctrl.set(new schema.All(Todo), 42); // ❌ completed is a boolean ctrl.set(Todo, { id: 5 }, { id: 5, completed: 'yes' }); // ❌ Todo has no done field ctrl.set(TodoResource.getList.schema, [{ id: 5, done: true }]); // ❌ updaters must return Todo fields ctrl.set(Todo, { id: 5 }, todo => ({ title: todo.completed })); }
Un Query recibe la entrada del schema que envuelve, ya que set() normaliza ese schema en lugar de
revertir process(). Cuando cada miembro declara su discriminador como un literal (como readonly type = 'post'), una
fila de Union se verifica contra el miembro que selecciona, así que no puede mezclar campos de miembros distintos; consulta Unions.
Si código que antes compilaba ahora falla aquí, estaba escribiendo datos que su schema no describe. Corrige el valor, o amplía los tipos de los campos de la Entity si los datos realmente pueden tener esa forma.
Eliminar una sola entidad con set()
Pasar un schema Invalidate a Controller.set() elimina la entidad que identifica su fila, pero la forma individual era un error de TypeScript, así que el código la envolvía en un lote de una fila. Ahora verifica los tipos correctamente, con la fila tipada según los campos de la Entity (#4230):
ctrl.set([new Invalidate(Todo)], [{ id: '5' }]);
ctrl.set(new Invalidate(Todo), { id: '5' });
Como el set() por lotes, no recibe args ni función actualizadora.
Params tipados de process()
Un método process() pasado a new RestEndpoint(),
.extend() o resource().extend() recibía params tipado como
any, así que leer un parámetro que el endpoint no tiene compilaba y devolvía undefined en tiempo de ejecución. params y
body ahora se tipan a partir del endpoint, incluido un path definido en la misma llamada a .extend()
(#4183, #4184):
import { RestEndpoint } from '@data-client/rest'; const getUser = new RestEndpoint({ path: '/users/:id' }); export const getUserById = getUser.extend({ path: '/users/by-id/:userId', process(value, params) { const userId: string | number = params.userId; // @ts-expect-error 'id' is not a param of '/users/by-id/:userId' params.id; return { ...value, userId }; }, });
Cuando todos los params son opcionales, params puede ser undefined, así que léelo con params?.page.
Esto puede hacer aparecer nuevos errores de TypeScript en métodos process() existentes. Cada uno marca una lectura que podría ser undefined
o lanzar una excepción en tiempo de ejecución, así que corrige el nombre del parámetro o añade la comprobación que falta.
Respuestas optimistas tipadas
getOptimisticResponse() podía devolver cualquier cosa a menos que el endpoint tuviera
un process() tipado, así que un tipo de campo incorrecto solo se manifestaba como datos erróneos en la interfaz hasta que el servidor respondía. El valor
de retorno ahora se verifica contra el schema del endpoint, como un valor tipado de set(): una Entity recibe cualquiera de sus
campos, y una Collection recibe una lista de ellos
(#4198):
import { Entity, RestEndpoint } from '@data-client/rest'; class Todo extends Entity { id = 0; title = ''; completed = false; } export const toggleTodo = new RestEndpoint({ path: '/todos/:id', method: 'PATCH', schema: Todo, // @ts-expect-error completed is a boolean; 'false' is truthy, so the checkbox would stay checked getOptimisticResponse(snap, { id }) { const todo = snap.get(Todo, { id }); if (!todo) throw snap.abort; return { id, completed: todo.completed ? 'false' : 'true' }; }, });
Esto puede hacer aparecer nuevos errores de TypeScript en métodos getOptimisticResponse() existentes. Cada uno marca un valor que se
almacenaría con el tipo incorrecto hasta que el servidor responda, así que devuelve el tipo real del campo (aquí !todo.completed).
push() tipado en Collections ordenadas
Envolver una Collection en un Query para ordenarla o filtrarla hacía que
push, unshift y remove rechazaran cualquier cuerpo excepto FormData, aunque sí agregan a la
lista en tiempo de ejecución. El cuerpo ahora se tipa a partir de la Entity de la Collection, así que la conversión de tipo puede eliminarse y un campo incorrecto es un
error de TypeScript. Lo mismo aplica a una Collection envuelta en Lazy
(#4226).
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';
export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author } as any);
}
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';
export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author });
}
donde getPosts ordena su Collection con un Query:
import { Collection, Entity, Query, RestEndpoint } from '@data-client/rest';
export class Post extends Entity {
id = '';
title = '';
group = '';
author = '';
}
export const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: 'title' | 'author' },
schema: new Query(
new Collection([Post], { nonFilterArgumentKeys: /orderBy/ }),
(posts, { orderBy }: { orderBy?: 'title' | 'author' } = {}) =>
orderBy ?
[...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy]))
: posts,
),
});
FormData en resource().create
El create de resource() envía un cuerpo FormData tal cual, como update, partialUpdate y
getList.push, pero su tipo solo permitía un objeto, así que enviar un formulario necesitaba una conversión de tipo
(#4235). Elimina la conversión:
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget) as any);
}}
>
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget));
}}
>
create está obsoleto; getList.push es su reemplazo y también acepta FormData.
Argumentos getter en Vue
Los composables de Vue como useSuspense() y useLive() estaban tipados para aceptar una
función getter como argumento, pero pasaban la propia función al endpoint en lugar de llamarla. Los getters ahora
funcionan igual que los refs y computed, y el composable vuelve a obtener los datos cuando cambia cualquier cosa que el getter lee
(#4115). Elimina el envoltorio computed() si solo lo usabas para hacer
reactivos los argumentos:
import { computed } from 'vue';
const props = defineProps<{ id: number }>();
const article = await useSuspense(
ArticleResource.get,
computed(() => ({ id: props.id })),
);
const props = defineProps<{ id: number }>();
const article = await useSuspense(ArticleResource.get, () => ({
id: props.id,
}));
Esto aplica a todos los composables que reciben argumentos de endpoint: useSuspense(), useLive(), useCache(), useDLE(), useFetch(), useQuery() y useSubscription().
Vue recolecta basura de forma predeterminada
El DataClientPlugin de Vue solo eliminaba los datos sin uso si pasabas un gcPolicy
tú mismo. Ahora usa new GCPolicy() de forma predeterminada, igual que el DataProvider de React, así que puedes
eliminar la opción a menos que la personalices (#4148).
import { DataClientPlugin, GCPolicy } from '@data-client/vue';
app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
import { DataClientPlugin } from '@data-client/vue';
app.use(DataClientPlugin);
Los datos se eliminan solo cuando ningún componente montado los lee y ha pasado el doble de su tiempo de frescura desde que se obtuvieron (al menos dos minutos). Un barrido comprueba cada cinco minutos. Todo lo que un componente vuelva a leer antes de eso vuelve a estar en uso.
Un componente que se monta después de la eliminación trata los datos como si nunca se hubieran obtenido: useSuspense() los
obtiene de nuevo, mientras que useCache() devuelve undefined. Para conservar los datos sin uso más tiempo o barrer con menos frecuencia,
pasa tu propio gcPolicy.
Stale-while-revalidate en Vue
Cuando un componente se monta con datos que están obsoletos pero siguen siendo válidos, el
useSuspense() de Vue ahora los renderiza de inmediato y vuelve a obtenerlos en segundo plano, como en React. Antes,
mostraba el fallback de <Suspense> hasta que terminaba la nueva obtención (#4169).
Las clases Entity verifican tipos como EntityInterface
Las funciones auxiliares tipadas para aceptar cualquier Entity con EntityInterface rechazaban las clases Entity, porque
Entity.pk() tipaba sus args como un array mutable. Ahora es readonly any[], así que esto
verifica los tipos correctamente (#4149):
import { Entity } from '@data-client/rest'; import type { EntityInterface } from '@data-client/react'; class User extends Entity { id = ''; name = ''; } function entityName(schema: EntityInterface) { return schema.key; } entityName(User);
Si sobrescribes static pk() y tipas args como un array mutable, todavía compila, pero una futura versión con cambios incompatibles
exigirá readonly; actualízalo ahora.
Las entidades eliminadas se leen como undefined
Cuando una entidad se eliminaba y su nueva obtención fallaba, useCache() y useDLE()
(React y Vue) devolvían un Symbol interno como datos. Un Symbol es verdadero, así que la comprobación habitual de "todavía no cargado" lo dejaba
pasar y el componente se renderizaba como si tuviera una entidad
(#4150):
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
// a deleted todo used to get past this guard, so todo.title.trim() threw
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
Ahora data es undefined, en consonancia con su tipo y con Controller.get(). Lo mismo aplica a
Controller.getResponse() y
Controller.fetchIfStale(), por ejemplo en Managers personalizados.
Si tenías una solución alternativa para esto, puedes eliminarla. Una simple comprobación de veracidad es suficiente:
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo || typeof todo === 'symbol') return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
Para mostrar algo específico cuando la nueva obtención falló (por ejemplo, un 404 tras la eliminación), lee error de
useDLE() en lugar de inspeccionar data.
Redux DevTools más rápido
Con la extensión Redux DevTools abierta, cada actualización del store en desarrollo
serializa todo el store para la extensión, y cada marca de tiempo en él se formateaba de la manera lenta. Los stores grandes o las
actualizaciones frecuentes, como el sondeo (polling), los datos en vivo o muchas llamadas a controller.set(), hacían que la página se entrecortara. Cada actualización ahora
se serializa entre 40 y 60 veces más rápido, y las marcas de tiempo siguen leyéndose como 10:42:07.123 AM
(#4163). Los builds de producción no incluyen DevTools, así que no se ven
afectados.
Guía de migración
Esta actualización requiere actualizar todas las versiones de los paquetes al mismo tiempo.
- NPM
- Yarn
- pnpm
- esm.sh
npm install --save @data-client/react@^0.19.0 @data-client/rest@^0.19.0 @data-client/endpoint@^0.19.0 @data-client/core@^0.19.0 @data-client/vue@^0.19.0 @data-client/test@^0.19.0 @data-client/img@^0.19.0
yarn add @data-client/react@^0.19.0 @data-client/rest@^0.19.0 @data-client/endpoint@^0.19.0 @data-client/core@^0.19.0 @data-client/vue@^0.19.0 @data-client/test@^0.19.0 @data-client/img@^0.19.0
pnpm add @data-client/react@^0.19.0 @data-client/rest@^0.19.0 @data-client/endpoint@^0.19.0 @data-client/core@^0.19.0 @data-client/vue@^0.19.0 @data-client/test@^0.19.0 @data-client/img@^0.19.0
<script type="importmap">
{
"imports": {
"@data-client/react": "https://esm.sh/@data-client/react@^0.19.0",
"@data-client/rest": "https://esm.sh/@data-client/rest@^0.19.0",
"@data-client/endpoint": "https://esm.sh/@data-client/endpoint@^0.19.0",
"@data-client/core": "https://esm.sh/@data-client/core@^0.19.0",
"@data-client/vue": "https://esm.sh/@data-client/vue@^0.19.0",
"@data-client/test": "https://esm.sh/@data-client/test@^0.19.0",
"@data-client/img": "https://esm.sh/@data-client/img@^0.19.0"
}
}
</script>
TypeScript 4.0 o posterior
Omite esta sección si ya usas TypeScript 4.0 o posterior.
Se eliminan las declaraciones para TypeScript 3.x, ya que dejaron de verificar tipos correctamente en todas las versiones de TypeScript 3.x. Sube
typescript a ^4.0.0 o posterior en tus devDependencies. #4151
Prepárate para versiones futuras
Nada de esto es necesario para actualizar a 0.19; tu código sigue funcionando tal cual. Cada elemento se romperá en una versión futura, así que actualizarlo ahora convierte esa actualización en algo trivial.
Los args de Entity.pk() son readonly
Omite esto si no sobrescribes static pk() con un parámetro args tipado.
Entity.pk() ahora tipa args como readonly any[]. Las sobrescrituras que lo anotan como un array
mutable todavía compilan por ahora, pero una versión futura exigirá readonly.
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: readonly any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}
createDataClient() de Vue
Omite esto si instalas el store con app.use(DataClientPlugin).
createDataClient() y ProvidedDataClient son internos de DataClientPlugin y
dejarán de exportarse. Instala el plugin en su lugar, y lee el controller con
useController(). #4193
import { createDataClient } from '@data-client/vue';
const { controller } = createDataClient({ managers, initialState });
import { DataClientPlugin, useController } from '@data-client/vue';
app.use(DataClientPlugin, { managers, initialState });
// in a component's setup()
const controller = useController();
waitForNextUpdate() de renderDataCompose() en Vue
Omite esto si tus pruebas de Vue no llaman a waitForNextUpdate().
waitForNextUpdate() de renderDataCompose() se rinde
en silencio después de 1 segundo, así que una prueba puede pasar mientras el composable sigue suspendido. Se eliminará. Usa await sobre el
resultado en su lugar, y llama a allSettled() después de cambiar props o llamar al controller; ahora también espera la
obtención que inicia un cambio de prop. #4221
const { result, waitForNextUpdate } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
await waitForNextUpdate();
const article = await result;
props.id = 6;
await waitForNextUpdate();
const { result, allSettled } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
const article = await result;
props.id = 6;
await allSettled();
Soporte para la actualización
Como siempre, si tienes algún problema o pregunta, no dudes en unirte a nuestro o reportar un error
