Pular para o conteúdo principal

v0.19: Controller.set() em lote, TypeScript mais rápido

· 28 min para ler
Nathaniel Tucker
Creator of Reactive Data Client
observação

O original em inglês mudou depois desta tradução, então ela pode estar desatualizada.

A v0.19 permite que um Manager escreva um lote inteiro de linhas recebidas por stream em uma única atualização da store, verifica os tipos do código de endpoints cerca de 2x mais rápido e corrige uma rodada de problemas de TypeScript e Vue. Coding agents também ganham um plugin do Claude Code, skills de Vue e llms.txt; veja Data Client para Coding Agents.

Novas APIs:

Desempenho:

Outras melhorias:

  • Corrige a resolução de módulos do TypeScript 7 para os exports do pacote; os imports agora resolvem para os arquivos de declaração (#4019)
  • renderDataHook() executa os efeitos de montagem do provider quando a primeira renderização suspende (#4099)
  • Os valores de Controller.set() são tipados pelo schema, então ctrl.set(new schema.All(Todo), 42) é um erro de TypeScript (#4133)
  • Controller.set() exclui uma entidade com um schema Invalidate sem a gambiarra do lote de uma linha (#4230)
  • process() recebe params tipados em new RestEndpoint(), .extend() e resource().extend(), então um parâmetro de path errado é um erro de TypeScript em vez de undefined em tempo de execução (#4183, #4184)
  • Os retornos de getOptimisticResponse() são tipados pelo schema, então um tipo de campo errado é um erro de TypeScript em vez de um flash de dados incorretos (#4198)
  • useSuspense() e useLive() do Vue mantêm os dados anteriores enquanto novos argumentos carregam, em vez de retornar undefined (#4131)
  • useSuspense() e useLive() do Vue enviam erros de fetch após a mudança de argumentos para onErrorCaptured() em vez de uma rejeição de promise não tratada (#4135)
  • useSuspense(), useDLE() e useFetch() do Vue não buscam mais novamente dados desatualizados a cada atualização da store, então um controller.set() não é sobrescrito (#4134)
  • useSuspense() do Vue mostra dados desatualizados na montagem e busca novamente em segundo plano, como no React, em vez de mostrar o fallback do <Suspense> até a nova busca terminar (#4169)
  • useDLE() e useCache() do Vue mantêm dados invalidIfStale expirados durante atualizações não relacionadas da store, em vez de ficarem travados carregando (#4142)
  • O DataClientPlugin do Vue é instalado em versões do Vue anteriores à 3.5 em vez de lançar app.onUnmount is not a function (#4146)
  • O DataClientPlugin do Vue faz coleta de lixo por padrão, igual ao DataProvider do React, então você não precisa mais passar gcPolicy; como ele decide o que remover (#4148)
  • Os composables do Vue aceitam argumentos getter como () => ({ id: props.id }); eles eram tipados para permiti-los, mas passavam a própria função ao endpoint (#4115)
  • Corrige os erros Cannot find name 'NoInfer' e export type no TypeScript 4.x com skipLibCheck desativado (#4138)
  • Corrige erros de tipo de Entity, Endpoint, Union e RestEndpoint no TypeScript 4.0–4.5 com skipLibCheck desativado (#4140)
  • Corrige erros ',' expected ao importar @data-client/react/redux, @data-client/react/nextjs ou @data-client/test no TypeScript 4.0–4.4 (#4232)
  • Corrige act e renderHook de @data-client/test tipados como any no TypeScript 4.0–4.4 (#4251)
  • Classes Entity podem ser usadas onde um EntityInterface é esperado (#4149); prepare as sobrescritas de pk() para um lançamento futuro
  • useCache() e useDLE() retornam undefined para uma entidade excluída cuja nova busca falhou, em vez de um Symbol truthy que passava por verificações if (!data); remova quaisquer gambiarras (#4150)
  • useFetch() do Vue impede que seus dados sejam coletados pelo garbage collector enquanto está montado, como o useSuspense(), então um gcPolicy configurado não remove mais dados pré-carregados que componentes leem depois (#4152)
  • Apps com o Redux DevTools aberto não travam mais em desenvolvimento com stores grandes ou atualizações frequentes; cada atualização serializa 40-60x mais rápido (#4163)
  • resource().extend() com um objeto de sobrescritas de endpoints mantém em seu tipo os endpoints adicionados antes com .extend('name', options), então usá-los não é mais um erro de TypeScript (#4184)
  • RestEndpoint.push, unshift e remove aceitam os campos da Entity como body quando um Query ou Lazy envolve a Collection, em vez de apenas FormData (#4226)
  • useFetch() do Vue é tipado como o Ref somente leitura que retorna, então promise.resolved (sempre undefined) agora é um erro de TypeScript; leia promise.value.resolved em vez disso (#4114)
  • Uma subclasse de Controller passada ao DataProvider ou ao DataClientPlugin não precisa mais de um cast para passar na verificação de tipos (#4228)
  • A store de prepareStore() pode ser passada ao <Provider> do react-redux sem cast, e tem dispatch() em seu tipo (#4238)
  • resource().create aceita FormData sem cast, como update, partialUpdate e getList.push (#4235)
  • RestEndpoint.push, unshift e remove enviam um body em array como JSON, então criar ou remover vários itens de uma vez não envia mais "[object Object]" ao seu servidor; resource().create também aceita um array (#4262)
  • O createDataClient() do Vue está descontinuado, já que é interno ao DataClientPlugin; instale o plugin com app.use(DataClientPlugin, options) em vez de chamá-lo diretamente (#4193)
  • O waitForNextUpdate() do renderDataCompose() do Vue está descontinuado, já que desiste silenciosamente após 1 segundo e deixa um teste passar enquanto ainda está suspenso; use await result em vez disso, ou await allSettled() após uma mudança, que agora também espera as buscas iniciadas por uma mudança de prop (testes unitários de composables, #4221)

Mudanças incompatíveis:

Prepare-se para lançamentos futuros:

Controller.set() em lote​

Um Manager que recebe um stream de linhas (como cotações de preços por um websocket) agora pode escrever todas elas com um único Controller.set() passando um schema Array. Antes da v0.19, [Ticker] era um erro de TypeScript, então a gambiarra comum era um set() por linha:

Before
ws.onmessage = event => {
const rows = JSON.parse(event.data);
for (const row of rows) {
ctrl.set(Ticker, { product_id: row.product_id }, row);
}
};
After
ws.onmessage = event => {
const rows = JSON.parse(event.data);
ctrl.set([Ticker], rows);
};

Cada linha é mesclada com sua entidade armazenada, e as entidades que não estão na lista ficam intactas. Schemas Array não recebem args nem função de atualização. Para agrupar em lote tipos de Entity diferentes, exclusões ou linhas indexadas por id, passe um schema Union, Invalidate ou Values; veja Controller.set() para exemplos. #4103

Experimente os dois botões abaixo. Esta verificação no navegador parte de um store vazio e mede o tempo de um Promise.all com 500 chamadas de set() em comparação com um único set() em lote. Ambos os caminhos resultam em um único commit do React, e cada um grava 500 preços novos.

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 />);
Resultado

Desempenho​

Cada set() é uma atualização separada da store, e toda atualização da store copia a tabela daquele tipo de entidade. Escrever as linhas uma de cada vez repete essa cópia para cada linha, então o custo cresce tanto com o tamanho do lote quanto com o tamanho da store. Um lote paga essa cópia uma única vez.

O gráfico é o benchmark setMany em node de examples/benchmark/core.js: uma store que já contém 500 entidades, depois um set() síncrono por linha comparado a um único set([Ticker], rows). Ele mede a atualização da store: 20x mais rápido para 50 linhas e 95x mais rápido para 500 linhas. #4103

setMany em Node em uma store com 500 entidades
50 rows20xOne set() per row 10.8 ms → set([Ticker], rows) 0.54 ms
500 rows95xOne set() per row 103 ms → set([Ticker], rows) 1.08 ms

Benchmarks ao longo do tempo | Ver o benchmark setMany

Em um Manager, acumule as mensagens recebidas em um buffer e descarregue cada lote com um único set(), como descrito em Agrupando atualizações de alta frequência em lote. O StreamManager do app de moedas agora descarrega as mensagens de ticker da Coinbase dessa forma.

StreamManager.ts
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]));
}
};

Explore o exemplo coin-app

Mais demos

dica

Se você filtra as ações do DevToolsManager por schema, o action.schema de uma escrita em lote é o schema Array, então compare action.schema[0] para [Ticker] em vez da própria Entity.

TypeScript mais rápido​

O TypeScript verifica novamente seu código a cada tecla digitada no editor e a cada build de CI. Em apps com muitos endpoints, tipos pesados de biblioteca aparecem como autocomplete lento, sublinhados vermelhos que levam segundos para aparecer e builds mais demorados. A v0.19 torna RestEndpoint, resource() e .extend() muito mais baratos de verificar, sem mudanças de código do seu lado. O TypeScript continua reportando todos os erros que reportava antes (#4173).

Resultados​

Nosso teste de estresse mais pesado, um arquivo com 150 RestEndpoints com paths longos, .extend() e .paginated(), agora é verificado 2x mais rápido no TypeScript 6 e 2,2x mais rápido no TypeScript 7, usando cerca de 40% menos memória. Todos os números comparam os pacotes publicados da v0.18.1 com a v0.19; os tempos de verificação são o melhor de 10 execuções.

Verificando 150 RestEndpoints com paths longos
TypeScript 61.9xv0.18 3.83 s → v0.19 1.98 s
TypeScript 72.2xv0.18 1.5 s → v0.19 0.69 s
Memória para o mesmo arquivo
TypeScript 61.7xv0.18 365 MB → v0.19 211 MB
TypeScript 71.7xv0.18 220 MB → v0.19 128 MB

Código com muitos endpoints faz muito menos trabalho de tipos. Instanciações de tipo são a unidade de trabalho do TypeScript: elas são determinísticas, então podem ser comparadas com precisão entre máquinas. Union faz mais trabalho do que na v0.18, porque a v0.19 agora verifica os tipos dos valores de set() (veja abaixo).

Instanciações de tipo por teste de estresse
Long paths2.4xv0.18 822 K → v0.19 341 K
Typical app1.5xv0.18 20.2 K → v0.19 13.4 K
React hooks1.2xv0.18 145 K → v0.19 122 K
Vue1.3xv0.18 62.6 K → v0.19 47.3 K
300 fields1.3xv0.18 17 K → v0.19 12.8 K
Schemas1.2xv0.18 118 K → v0.19 99.3 K
Union0.6xv0.18 9 K → v0.19 16.1 K

Passe o mouse ou toque em uma célula para ver os números exatos.

TS 6 check timeTS 7 check timeTS 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

As linhas de set() medem os valores tipados de set(), que a v0.18 não verificava de forma alguma. Valores simples ainda são verificados mais rápido do que antes, e funções de atualização em Unions grandes são verificadas em aproximadamente o mesmo tempo, mesmo que o TypeScript agora verifique o valor de retorno de cada função de atualização (#4179).

Arquivos pequenos são dominados pelo custo fixo de inicialização do TypeScript (carregar lib.dom.d.ts sozinho leva cerca de 100MB), então seu tempo e memória quase não mudam. A economia se acumula conforme a base de código cresce.

O que mudou​

  • .extend() e .paginated() são compartilhados entre todos os endpoints em vez de recriados para cada tipo de endpoint.
  • As opções de endpoint são inferidas como tipos de objeto simples, então o TypeScript para de reconstruí-las a cada uso.
  • Parâmetros de path como /users/:id são lidos em uma única passagem.

Antes de publicar, desativamos todos os @ts-expect-error da suíte de testes no TypeScript 4.0 até o 7 e confirmamos que todos os erros continuam aparecendo no mesmo lugar.

No TypeScript 5.x e anteriores, um método process(value, params) passado ao .extend() também não falha mais com "implicitly has an 'any' type" em modo strict.

Outras melhorias​

Valores tipados de set()​

Controller.set() antes aceitava qualquer valor para um schema, então um erro de digitação ou um tipo de campo errado só aparecia como dados incorretos em tempo de execução. Os valores agora são tipados pelo schema: uma Entity recebe seus campos, enquanto uma Collection, All ou Array recebe uma lista de linhas. Todo campo é opcional, já que set() mescla com o que já está armazenado, e números e strings são intercambiáveis assim como nas respostas de API. #4133

Passe o mouse sobre os sublinhados vermelhos para ver cada erro.

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

Um Query recebe a entrada do schema que envolve, já que set() normaliza esse schema em vez de reverter o process(). Quando cada membro declara seu discriminador como um literal (como readonly type = 'post'), uma linha de Union é verificada contra o membro que ela seleciona, então não pode misturar campos de membros diferentes; veja Unions.

Se um código que antes compilava agora falha aqui, ele estava escrevendo dados que seu schema não descreve. Corrija o valor, ou amplie os tipos dos campos da Entity se os dados realmente puderem ter essa forma.

Exclua uma entidade com set()​

Passar um schema Invalidate ao Controller.set() exclui a entidade que sua linha identifica, mas a forma singular era um erro de TypeScript, então o código a envolvia em um lote de uma linha. Agora ela passa na verificação de tipos, com a linha tipada pelos campos da Entity (#4230):

Before
ctrl.set([new Invalidate(Todo)], [{ id: '5' }]);
After
ctrl.set(new Invalidate(Todo), { id: '5' });

Como o set() em lote, ela não recebe args nem função de atualização.

Params tipados em process()​

Um método process() passado a new RestEndpoint(), .extend() ou resource().extend() recebia params tipado como any, então ler um parâmetro que o endpoint não tem compilava e retornava undefined em tempo de execução. params e body agora são tipados a partir do endpoint, incluindo um path definido na mesma chamada de .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 };
  },
});

Quando todos os params são opcionais, params pode ser undefined, então leia-o com params?.page.

Isso pode revelar novos erros de TypeScript em métodos process() existentes. Cada um marca uma leitura que poderia ser undefined ou lançar uma exceção em tempo de execução, então corrija o nome do param ou adicione a verificação que falta.

Respostas otimistas tipadas​

getOptimisticResponse() podia retornar qualquer coisa, a menos que o endpoint tivesse um process() tipado, então um tipo de campo errado só aparecia como dados incorretos na interface até o servidor responder. O valor de retorno agora é verificado contra o schema do endpoint, como um valor tipado de set(): uma Entity recebe qualquer um de seus campos, e uma Collection recebe uma lista deles (#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' };
  },
});

Isso pode revelar novos erros de TypeScript em métodos getOptimisticResponse() existentes. Cada um marca um valor que seria armazenado com o tipo errado até o servidor responder, então retorne o tipo real do campo (aqui, !todo.completed).

push() tipado em Collections ordenadas​

Envolver uma Collection em um Query para ordená-la ou filtrá-la fazia push, unshift e remove rejeitarem qualquer body exceto FormData, embora adicionem à lista em tempo de execução. O body agora é tipado a partir da Entity da Collection, então o cast pode ser removido e um campo errado é um erro de TypeScript. O mesmo vale para uma Collection envolvida em Lazy (#4226).

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

onde getPosts ordena sua Collection com um Query:

getPosts.ts
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 em resource().create​

O create de resource() envia um body FormData como está, assim como update, partialUpdate e getList.push, mas seu tipo só permitia um objeto, então enviar um formulário exigia um cast (#4235). Remova o cast:

Before
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget) as any);
}}
>
After
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget));
}}
>

create está descontinuado; getList.push é seu substituto e também aceita FormData.

Argumentos getter no Vue​

Composables do Vue como useSuspense() e useLive() eram tipados para aceitar uma função getter como argumento, mas passavam a própria função ao endpoint em vez de chamá-la. Getters agora funcionam da mesma forma que refs e computed, e o composable busca novamente quando qualquer coisa que o getter lê muda (#4115). Remova o computed() ao redor se você só o usava para tornar os argumentos reativos:

Before
import { computed } from 'vue';

const props = defineProps<{ id: number }>();
const article = await useSuspense(
ArticleResource.get,
computed(() => ({ id: props.id })),
);
After
const props = defineProps<{ id: number }>();
const article = await useSuspense(ArticleResource.get, () => ({
id: props.id,
}));

Isso vale para todo composable que recebe argumentos de endpoint: useSuspense(), useLive(), useCache(), useDLE(), useFetch(), useQuery() e useSubscription().

Vue faz coleta de lixo por padrão​

O DataClientPlugin do Vue só removia dados não utilizados se você mesmo passasse um gcPolicy. Agora o padrão é new GCPolicy(), o mesmo do DataProvider do React, então você pode remover a opção, a menos que a personalize (#4148).

Before
import { DataClientPlugin, GCPolicy } from '@data-client/vue';

app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
After
import { DataClientPlugin } from '@data-client/vue';

app.use(DataClientPlugin);

Os dados só são removidos quando nenhum componente montado os lê e o dobro do seu tempo de vida de frescor passou desde que foram buscados (no mínimo dois minutos). Uma varredura verifica a cada cinco minutos. Qualquer coisa que um componente leia novamente antes disso volta a estar em uso.

Um componente que monta após a remoção trata os dados como nunca buscados: o useSuspense() os busca de novo, enquanto o useCache() retorna undefined. Para manter dados não utilizados por mais tempo ou varrer com menos frequência, passe seu próprio gcPolicy.

Stale-while-revalidate no Vue​

Quando um componente monta com dados que estão desatualizados mas ainda válidos, o useSuspense() do Vue agora os renderiza imediatamente e busca novamente em segundo plano, como no React. Antes, ele mostrava o fallback do <Suspense> até a nova busca terminar (#4169).

Classes Entity passam na verificação de tipos como EntityInterface​

Funções auxiliares tipadas para aceitar qualquer Entity com EntityInterface rejeitavam classes Entity, porque Entity.pk() tipava seus args como um array mutável. Agora é readonly any[], então isto passa na verificação de tipos (#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);

Se você sobrescreve static pk() e tipa args como um array mutável, ainda compila, mas um futuro lançamento com mudanças incompatíveis exigirá readonly; atualize agora.

Entidades excluídas são lidas como undefined​

Quando uma entidade era excluída e sua nova busca falhava, useCache() e useDLE() (React e Vue) retornavam um Symbol interno como dados. Um Symbol é truthy, então a verificação comum de "ainda não carregou" o deixava passar e o componente renderizava como se tivesse uma entidade (#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>;
}

Agora data é undefined, de acordo com seu tipo e com Controller.get(). O mesmo vale para Controller.getResponse() e Controller.fetchIfStale(), por exemplo em Managers personalizados.

Se você contornou isso, a gambiarra pode ser removida. Uma simples verificação de truthiness basta:

Before
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo || typeof todo === 'symbol') return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
After
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 quando a nova busca falhou (por exemplo, um 404 após a exclusão), leia error de useDLE() em vez de inspecionar data.

Redux DevTools mais rápido​

Com a extensão Redux DevTools aberta, toda atualização da store em desenvolvimento serializa a store inteira para a extensão, e cada timestamp nela era formatado do jeito lento. Stores grandes ou atualizações frequentes, como polling, dados em tempo real ou muitas chamadas de controller.set(), faziam a página travar. Cada atualização agora serializa 40-60x mais rápido, e os timestamps continuam aparecendo como 10:42:07.123 AM (#4163). Builds de produção não incluem o DevTools, então não são afetados.

Serialização do DevTools por atualização da store
50 entities60xv0.18 18.6 ms → v0.19 0.31 ms
500 entities40xv0.18 201 ms → v0.19 5 ms

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.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

TypeScript 4.0 ou posterior​

Pule esta seção se você já usa TypeScript 4.0 ou posterior.

As declarações para TypeScript 3.x foram removidas, já que não passavam mais na verificação de tipos em nenhuma versão do TypeScript 3.x. Atualize typescript para ^4.0.0 ou posterior nas suas devDependencies. #4151

Prepare-se para lançamentos futuros​

Nada aqui é necessário para atualizar para a 0.19; seu código continua funcionando como está. Cada item vai quebrar em um lançamento futuro, então atualizar agora torna aquela atualização trivial.

Os args de Entity.pk() são readonly​

Pule isto se você não sobrescreve static pk() com um parâmetro args tipado.

Entity.pk() agora tipa args como readonly any[]. Sobrescritas que o anotam como um array mutável ainda compilam por enquanto, mas um lançamento futuro exigirá readonly.

Before
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}
After
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: readonly any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}

createDataClient() do Vue​

Pule isto se você instala a store com app.use(DataClientPlugin).

createDataClient() e ProvidedDataClient são internos ao DataClientPlugin e deixarão de ser exportados. Em vez disso, instale o plugin e leia o controller com useController(). #4193

Before
import { createDataClient } from '@data-client/vue';

const { controller } = createDataClient({ managers, initialState });
After
import { DataClientPlugin, useController } from '@data-client/vue';

app.use(DataClientPlugin, { managers, initialState });
// in a component's setup()
const controller = useController();

waitForNextUpdate() do renderDataCompose() do Vue​

Pule isto se seus testes de Vue não chamam waitForNextUpdate().

O waitForNextUpdate() de renderDataCompose() desiste silenciosamente após 1 segundo, então um teste pode passar enquanto o composable ainda está suspenso. Ele será removido. Em vez disso, use await no resultado e chame allSettled() após mudar props ou chamar o controller; agora ele também espera a busca iniciada por uma mudança de prop. #4221

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

Suporte à atualização​

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