Pular para o conteúdo principal

v0.15: Vue 3, Collection.remove, atualização do useDebounce

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

Novas plataformas:

  • Vue 3 com composables completos: useSuspense, useCache, useDLE, useController, useQuery

Novos recursos:

Desempenho:

Mudanças incompatíveis:

Atualize com um codemod automatizado para as mudanças incompatíveis mais comuns:

npx jscodeshift -t https://dataclient.io/codemods/v0.15.js --extensions=ts,tsx,js,jsx src/

Suporte a Vue 3​

O novo pacote @data-client/vue traz todo o poder do Reactive Data Client para o Vue 3 com composables nativos. #3549, #3585

Instalação​

npm install @data-client/vue @data-client/rest

Configuração​

Instale o DataClientPlugin no seu app:

main.ts
import { createApp } from 'vue';
import { DataClientPlugin } from '@data-client/vue';

const app = createApp(App);
app.use(DataClientPlugin);
app.mount('#app');

Composables​

Todos os hooks já conhecidos estão disponíveis como composables do Vue:

ArticleDetail.vue
<script setup lang="ts">
import { useSuspense } from '@data-client/vue';
import { ArticleResource } from './resources';

const props = defineProps<{ id: string }>();

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

<template>
<article>
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
</article>
</template>

Composables disponíveis:

Testes com MockPlugin​

test setup
import { createApp } from 'vue';
import { DataClientPlugin } from '@data-client/vue';
import { MockPlugin } from '@data-client/vue/test';

const app = createApp(App);
app.use(DataClientPlugin);
app.use(MockPlugin, {
fixtures: [
{
endpoint: MyResource.get,
args: [{ id: 1 }],
response: { id: 1, name: 'Test' },
},
],
});

Collection.remove​

Novo schema para remover itens de Collections sem excluir a entidade em si. #3560

import { schema } from '@data-client/rest';

const TodoCollection = new schema.Collection([Todo]);
const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
}).extend({
getList: { schema: TodoCollection },
});

// Remove from collection without deleting
ctrl.set(TodoResource.getList.schema.remove, { id: '123' });

Isso é útil quando um item deve ser removido de uma visualização de lista, mas continuar existindo em outros lugares:

// Remove user from a specific group list
ctrl.set(UserResource.getList.schema.remove, { id: userId, group: 'admins' });

RestEndpoint.remove​

RestEndpoint.remove combina uma atualização PATCH com a remoção da collection - perfeito para operações de "arquivar" ou "mover". #3623

const getTodos = new RestEndpoint({
path: '/todos',
schema: new schema.Collection([Todo]),
});

// Removes Todo from collection AND updates it with new data
await ctrl.fetch(
getTodos.remove,
{},
{ id: '123', title: 'Done', completed: true, archived: true }
);
// Move user between groups
await ctrl.fetch(
UserResource.getList.remove,
{ group: 'team-a' },
{ id: 2, username: 'bob', group: 'team-b' }
);
// User removed from 'team-a' list AND entity updated with group: 'team-b'

Consultas a Union​

Unions agora podem ser consultadas sem especificar o discriminador de tipo. #3558

Before
// Type discriminator required
// @ts-expect-error - missing 'type'
const event = useQuery(EventUnion, { id });
// event is undefined

const newsEvent = useQuery(EventUnion, { id, type: 'news' });
// newsEvent is found
After
// Works without type discriminator
const event = useQuery(EventUnion, { id });
// event is found!

const newsEvent = useQuery(EventUnion, { id, type: 'news' });
// newsEvent is also found

Invalidate dá suporte a Unions​

schema.Invalidate agora aceita schemas Union para operações de exclusão polimórficas. Além disso, resource().delete envolve automaticamente schemas Union com Invalidate. #3559

const FeedUnion = new schema.Union(
{ posts: Post, comments: Comment },
'type',
);

// resource() automatically handles Union delete
const FeedResource = resource({
path: '/feed/:id',
schema: FeedUnion,
});
await ctrl.fetch(FeedResource.delete, { id: '123' });

// For standalone endpoints, use schema.Invalidate directly
const deleteFeedItem = new RestEndpoint({
path: '/feed/:id',
method: 'DELETE',
schema: new schema.Invalidate(FeedUnion),
});
await ctrl.fetch(deleteFeedItem, { id: '123' });

Utilitários de teste​

mockInitialState()​

Novo utilitário para criar um estado pré-populado em testes. a4092a1

import { mockInitialState } from '@data-client/react/mock';
import { ArticleResource } from './resources';

const state = mockInitialState([
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'Hello', content: 'World' },
},
]);

@data-client/core/mock​

Novo ponto de entrada de mock com utilitários para construir implementações de mock personalizadas:

import {
MockController,
collapseFixture,
createFixtureMap,
} from '@data-client/core/mock';
  • MockController - Wrapper de Controller para mockar endpoints
  • collapseFixture - Resolve respostas de fixtures (trata respostas em forma de função)
  • createFixtureMap - Separa fixtures em um mapa estático e interceptors

Desempenho​

Remover a sobrecarga da detecção automática de ImmutableJS e otimizar os caminhos de código da desnormalização proporciona uma melhoria de 10 a 20% nas operações de get/denormalize. #3421, #3468

Benchmarks ao longo do tempo | Ver benchmark

Outras melhorias​

  • Corrige os tipos de getPage quando paginationField está no body
  • Corrige o tratamento polimórfico de entidades invalidadas em schema.All()
  • Corrige os tipos do argumento 'state' de controller.get e controller.getQueryMeta

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.15.0 @data-client/rest@^0.15.0 @data-client/test@^0.15.0 @data-client/img@^0.15.0

Um codemod automatizado trata as mudanças incompatíveis abaixo:

npx jscodeshift -t https://dataclient.io/codemods/v0.15.js --extensions=ts,tsx,js,jsx src/

useDebounce() retorna [val, isPending]​

useDebounce() agora retorna uma tupla com o estado pendente, seguindo o padrão do useTransition do React 19. #3459

Before
import { useDebounce } from '@data-client/react';

const debouncedQuery = useDebounce(query, 100);
After
import { useDebounce } from '@data-client/react';

const [debouncedQuery] = useDebounce(query, 100);
// Or use the pending state
const [debouncedQuery, isPending] = useDebounce(query, 100);
dica

A assinatura anterior continua disponível via @data-client/react/next para uma migração gradual.

ImmutableJS movido para os exports /imm​

O suporte a ImmutableJS não é mais detectado automaticamente. Use os exports explícitos /imm para lidar com estado em ImmutableJS. #3421, #3468

Before
import { MemoCache, denormalize } from '@data-client/normalizr';

const memo = new MemoCache();
// Auto-detected ImmutableJS
After
import { MemoCache } from '@data-client/normalizr';
import { MemoPolicy, denormalize, normalize } from '@data-client/normalizr/imm';

const memo = new MemoCache(MemoPolicy);

O ponto de entrada /imm agora inclui tanto normalize() quanto denormalize() para o tratamento completo de estado em ImmutableJS:

import { normalize } from '@data-client/normalizr/imm';
import { fromJS } from 'immutable';

const result = normalize(Article, responseData, args, {
entities: fromJS({}),
indexes: fromJS({}),
entitiesMeta: fromJS({}),
});
// result.entities is an ImmutableJS Map

Essa mudança proporciona uma melhoria de desempenho de 10 a 20% ao remover a sobrecarga da detecção de ImmutableJS para quem não o utiliza.

Mudanças na interface de delegate dos Schemas​

Se você tem schemas personalizados que sobrescrevem normalize() ou queryKey(), os argumentos de callback foram consolidados em um objeto delegate. #3449

normalize()​

Before
normalize(
input: any,
parent: any,
key: string | undefined,
args: any[],
visit: (...args: any) => any,
addEntity: any,
getEntity: any,
checkLoop: any,
): string {
addEntity(this, processedEntity, id);
}
After
normalize(
input: any,
parent: any,
key: string | undefined,
args: any[],
visit: (...args: any) => any,
delegate: INormalizeDelegate,
): string {
delegate.mergeEntity(this, id, processedEntity);
}

queryKey()​

Before
queryKey(args, queryKey, getEntity, getIndex) {
getIndex(schema.key, indexName, value)[value];
getEntity(this.key, id);
return queryKey(this.schema, args, getEntity, getIndex);
}
After
queryKey(args, unvisit, delegate) {
delegate.getIndex(schema.key, indexName, value);
delegate.getEntity(this.key, id);
return unvisit(this.schema, args);
}

delegate.getEntity → delegate.getEntities #3501​

Ao iterar sobre todas as entidades de um tipo, use delegate.getEntities(), que retorna uma interface restrita com os métodos iteradores keys() e entries():

Before
const entities = delegate.getEntity(this.key);
if (entities)
Object.keys(entities).forEach(collectionPk => {
if (!filterCollections(JSON.parse(collectionPk))) return;
delegate.mergeEntity(this, collectionPk, normalizedValue);
});
After
const entities = delegate.getEntities(this.key);
if (entities)
for (const collectionKey of entities.keys()) {
if (!filterCollections(JSON.parse(collectionKey))) continue;
delegate.mergeEntity(this, collectionKey, normalizedValue);
}

Símbolo INVALID removido​

O símbolo INVALID não é mais exportado. Use os métodos do delegate em seu lugar. #3461

Before
import { INVALID } from '@data-client/endpoint';

normalize(..., delegate) {
delegate.setEntity(this, pk, INVALID);
}

queryKey(args, unvisit, delegate) {
if (!found) return INVALID;
}
After
normalize(..., delegate) {
delegate.invalidate({ key: this.key }, pk);
}

queryKey(args, unvisit, delegate) {
if (!found) return delegate.INVALID;
}

state.entityMeta -> state.entitiesMeta​

Propriedade do estado interno renomeada por consistência. Isso afeta apenas o acesso direto à store. #3451

Mudanças na estrutura de dados do NetworkManager​

Os campos fetched, rejectors, resolvers e fetchedAt do NetworkManager foram consolidados em fetching. #3394

Before
if (action.key in this.fetched)
After
if (this.fetching.has(action.key))

Mudanças na API do MemoCache (normalizr)​

Para quem usa @data-client/normalizr diretamente:

MemoCache.query() e buildQueryKey() recebem o state como um único argumento #3372​

Before
this.memo.buildQueryKey(schema, args, state.entities, state.indexes, key);
this.memo.query(schema, args, state.entities, state.indexes);
After
this.memo.buildQueryKey(schema, args, state, key);
this.memo.query(schema, args, state);

MemoCache.query() retorna { data, paths }​

const { data } = this.memo.query(schema, args, state);
return typeof data === 'symbol' ? undefined : (data as any);

Suporte à atualização​

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