v0.14: APIs de Manager simplificadas, desempenho e GC
Managers:
As APIs de Manager foram simplificadas — formatos de action mais planos reduzem a complexidade do código e melhoram o desempenho ao minimizar entradas distintas de cache inline, enquanto APIs de configuração mais simples tornam a personalização mais agradável.
- Formatos de action simplificados
- Propriedade Manager.middleware
- Configuração de getDefaultManagers()
Desempenho:
- Chamadas de fetch imediatas - sem mais esperar pelo idle
- Bundles 30% menores ao remover polyfills desnecessários
- Normalização 10% mais rápida, desnormalização 32% mais rápida
Schema/modelagem de dados:
- Joins com Query usando schemas de Object
- EntityMixin para compor o comportamento de Entity
- Padrão de Entity.pk() usa
id - Invalidação dinâmica via Entity.process()
Testes:
- renderDataHook() para testes mais simples
Coleta de lixo:
- GCPolicy - coleta de lixo automática de dados desatualizados
- NetworkManager busca imediatamente
- action.payload -> action.response
- action.meta.args -> action.args
Managers
As APIs de Manager foram simplificadas — formatos de action mais planos reduzem a complexidade do código e melhoram o desempenho ao minimizar entradas distintas de cache inline, enquanto APIs de configuração mais simples tornam a personalização mais agradável.
Formatos de action
Os formatos de action estão mais simples e mais consistentes. #3143, #3139
action.meta.args->action.argsaction.meta.key->action.keyaction.meta.nmremovido (não era usado)action.payload->action.response(para SET_RESPONSE)
{
"type": "rdc/setresponse",
"meta": {
"key": "PATCH https://jsonplaceholder.typicode.com/todos/1",
"payload": {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": true
},
"args": [
{ "id": 1 },
{ "completed": true }
],
"nm": true,
"endpoint": {
"name": "Todo.partialUpdate",
"schema": {
"key": "Todo",
"schema": {}
},
"sideEffect": true,
},
"fetchedAt": "12:44:24.206 PM",
"date": "12:44:24.334 PM",
"expiresAt": "1:44:24.334 PM"
},
"error": false
}
{
"type": "rdc/setresponse",
"key": "PATCH https://jsonplaceholder.typicode.com/todos/1",
"response": {
"userId": 1,
"id": 1,
"title": "delectus aut autem",
"completed": true
},
"args": [
{ "id": 1 },
{ "completed": true }
],
"endpoint": {
"name": "Todo.partialUpdate",
"schema": {
"key": "Todo",
"schema": {}
},
"sideEffect": true,
},
"meta": {
"fetchedAt": "12:44:24.206 PM",
"date": "12:44:24.334 PM",
"expiresAt": "1:44:24.334 PM"
},
"error": false
}
Manager.middleware
Managers agora podem usar uma propriedade middleware em vez do método getMiddleware(). #3164
class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
// handle action
return next(action);
};
cleanup() {}
}
class MyManager implements Manager {
middleware: Middleware = controller => next => async action => {
// handle action
return next(action);
};
cleanup() {}
}
getMiddleware() continua funcionando por compatibilidade retroativa.
actionTypes sem o sufixo _TYPE
Nomes de tipos de action mais limpos agora estão disponíveis (não é uma mudança incompatível - também mantemos os nomes antigos). #3244
import type { Manager, Middleware } from '@data-client/react';
import { actionTypes } from '@data-client/react';
export default class LoggingManager implements Manager {
middleware: Middleware = controller => next => async action => {
switch (action.type) {
case actionTypes.SET_RESPONSE_TYPE:
console.info(
`${action.endpoint.name} ${JSON.stringify(action.response)}`,
);
default:
return next(action);
}
};
cleanup() {}
}
import type { Manager, Middleware } from '@data-client/react';
import { actionTypes } from '@data-client/react';
export default class LoggingManager implements Manager {
middleware: Middleware = controller => next => async action => {
switch (action.type) {
case actionTypes.SET_RESPONSE:
console.info(
`${action.endpoint.name} ${JSON.stringify(action.response)}`,
);
default:
return next(action);
}
};
cleanup() {}
}
getDefaultManagers()
getDefaultManagers() agora aceita opções de configuração. #3161
import { getDefaultManagers } from '@data-client/react';
// Remove DevToolsManager
const managers = getDefaultManagers({ devToolsManager: null });
// Configure DevToolsManager
const managers = getDefaultManagers({
devToolsManager: {
latency: 1000,
predicate: (state, action) => action.type !== actionTypes.SET_RESPONSE,
},
});
// Use custom NetworkManager
const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});
Construtor do NetworkManager
O construtor do NetworkManager agora usa argumentos nomeados para maior clareza.
new NetworkManager(42, 7);
new NetworkManager({ dataExpiryLength: 42, errorExpiryLength: 7 });
Desempenho
O modo concorrente e o batching automático do React 18+ mudam como devemos otimizar. Esperar por callbacks de idle já não é o ideal — buscar imediatamente agora traz resultados melhores. Mudanças internas aproveitam closures e estruturas de dados Map para normalização e consultas mais rápidas, enquanto a remoção de dependências infladas de polyfill reduz significativamente os bundles.
Busca imediata
O NetworkManager agora busca imediatamente em vez de esperar por callbacks de idle. #3146
Isso resulta em carregamento de dados mais rápido, especialmente em aplicações com árvores de renderização complexas.
O IdlingNetworkManager continua disponível para quem preferir o comportamento anterior:
import { IdlingNetworkManager, getDefaultManagers } from '@data-client/react';
const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});
Tamanho do bundle
Os pacotes agora são 30% menores após a remoção de pacotes inflados de polyfill. #3384
Além disso, os polyfills não poluem mais o escopo global. #3353
Desempenho da normalização
Mudanças internas na interface de schema trazem um ganho de 10% no desempenho da normalização. #3134
Usar Map em vez de objetos simples para armazenar entidades oferece consultas até 32% mais rápidas em grandes conjuntos de dados. #3390
Schema/modelagem de dados
As APIs de schema reduzem o código boilerplate em padrões comuns e, ao mesmo tempo, habilitam casos de uso mais avançados. Joins no lado do cliente agora funcionam entre entidades arbitrárias (não apenas relações aninhadas), a invalidação dinâmica lida com lógica de exclusão que só existe no servidor, e padrões sensatos como Entity.pk() usando id eliminam código repetitivo.
Joins com Query
Query agora pode receber schemas de Object, permitindo joins entre vários tipos de entidade. #3165
class Ticker extends Entity {
product_id = '';
price = 0;
pk() { return this.product_id; }
}
class Stats extends Entity {
product_id = '';
last = 0;
pk() { return this.product_id; }
}
// Join Ticker and Stats by product_id
const queryPrice = new schema.Query(
{ ticker: Ticker, stats: Stats },
({ ticker, stats }) => ticker?.price ?? stats?.last,
);
// Use in components
const price = useQuery(queryPrice, { product_id: 'BTC-USD' });
EntityMixin
Novo EntityMixin para compor o comportamento de Entity com classes existentes. #3243
import { EntityMixin } from '@data-client/rest';
// Your existing class
export class Article {
id = '';
title = '';
content = '';
tags: string[] = [];
}
// Add Entity behavior
export class ArticleEntity extends EntityMixin(Article) {}
Padrão de Entity.pk()
Entity.pk() agora usa this.id por padrão. #3188
class Todo extends Entity {
id = '';
title = '';
completed = false;
pk() {
return this.id;
}
}
class Todo extends Entity {
id = '';
title = '';
completed = false;
}
Invalidação dinâmica
Retorne undefined de Entity.process() para invalidar entidades dinamicamente com base nos dados da resposta. #3407
class PriceLevel extends Entity {
price = 0;
amount = 0;
pk() { return `${this.price}`; }
static process(input: [number, number], parent: any, key: string | undefined) {
const [price, amount] = input;
// Invalidate when amount is 0
if (amount === 0) return undefined;
return { price, amount };
}
}
resource()
createResource() foi renomeado para resource() por brevidade. #3158
import { createResource } from '@data-client/rest';
const TodoResource = createResource({
path: '/todos/:id',
schema: Todo,
});
import { resource } from '@data-client/rest';
const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
});
createResource continua exportado por compatibilidade retroativa.
Testes
Testar deve ser simples. Novos helpers usam padrões sensatos para eliminar o código boilerplate em cenários de teste comuns.
renderDataHook()
O novo renderDataHook() usa o DataProvider padrão, eliminando código boilerplate. #3238
Além disso, makeRenderDataClient() foi renomeado para makeRenderDataHook() por consistência (o nome antigo continua funcionando).
import { renderDataHook } from '@data-client/test';
const { result } = renderDataHook(
() => useSuspense(ArticleResource.get, { id: 5 }),
{
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 5 }],
response: { id: 5, title: 'Test' },
},
],
},
);
Coleta de lixo
Aplicações de longa duração acumulam dados desatualizados na memória. Novas políticas de coleta de lixo limpam automaticamente entradas expiradas, usando contagem de referências para rastrear quais dados estão ativamente vinculados a componentes.
GCPolicy
GCPolicy habilita a coleta de lixo automática de dados desatualizados. #3343
import { GCPolicy, DataProvider } from '@data-client/react';
// Run GC sweep every 10 minutes
<DataProvider gcPolicy={new GCPolicy({ intervalMS: 60 * 1000 * 10 })}>
{children}
</DataProvider>
Configure o comportamento de expiração:
new GCPolicy({
intervalMS: 60 * 1000 * 5, // How often to run GC
expiryMultiplier: 2, // How many stale lifetimes before GC
})
Use ImmortalGCPolicy para desabilitar a coleta de lixo (comportamento padrão anterior):
import { ImmortalGCPolicy, DataProvider } from '@data-client/react';
<DataProvider gcPolicy={new ImmortalGCPolicy()}>
{children}
</DataProvider>
Outras melhorias
- Collections funcionam com schemas polimórficos como Union #3151
- Collections funcionam com args aninhados (compatibilidade com a biblioteca qs) #3281
- Interceptors funcionam em actions despachadas por managers #3365
- Mensagens de NetworkError incluem a URL para facilitar a depuração
- React Native:
InteractionManager.runAfterInteractionspara reduzir quedas de frames #3127 - React Native: entrada
react-nativenos exports do package.json #3371 - Compatibilidade aprimorada com React 19 #3279
Guia de migração
Esta atualização exige atualizar todas as versões dos pacotes simultaneamente.
- NPM
- Yarn
- pnpm
- esm.sh
npm install --save @data-client/react@^0.14.0 @data-client/rest@^0.14.0 @data-client/test@^0.14.0 @data-client/img@^0.14.0
yarn add @data-client/react@^0.14.0 @data-client/rest@^0.14.0 @data-client/test@^0.14.0 @data-client/img@^0.14.0
pnpm add @data-client/react@^0.14.0 @data-client/rest@^0.14.0 @data-client/test@^0.14.0 @data-client/img@^0.14.0
<script type="importmap">
{
"imports": {
"@data-client/react": "https://esm.sh/@data-client/react@^0.14.0",
"@data-client/rest": "https://esm.sh/@data-client/rest@^0.14.0",
"@data-client/test": "https://esm.sh/@data-client/test@^0.14.0",
"@data-client/img": "https://esm.sh/@data-client/img@^0.14.0"
}
}
</script>
Para todos os usuários
NetworkManager busca imediatamente
O NetworkManager agora busca imediatamente em vez de esperar pelo idle. Isso melhora o desempenho com React 18+. #3146
Para manter o comportamento anterior, use IdlingNetworkManager:
import { IdlingNetworkManager, getDefaultManagers } from '@data-client/react';
const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});
Para autores de Managers personalizados
Pule esta seção se você não tem Managers personalizados.
action.payload -> action.response
Para Managers personalizados que tratam SET_RESPONSE: #3141
import {
SET_RESPONSE_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';
export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case SET_RESPONSE_TYPE:
console.log('Resolved with value', action.payload);
return next(action);
default:
return next(action);
}
};
cleanup() {}
}
import {
SET_RESPONSE_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';
export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case SET_RESPONSE_TYPE:
console.log('Resolved with value', action.response);
return next(action);
default:
return next(action);
}
};
cleanup() {}
}
fetchAction.payload removido
Para Managers personalizados que tratam FETCH: #3141
import {
FETCH_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';
export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case FETCH_TYPE:
// consume fetch, and print the resolution
action.payload().then(response => console.log(response));
default:
return next(action);
}
};
cleanup() {}
}
import {
FETCH_TYPE,
type Manager,
type Middleware,
} from '@data-client/react';
export default class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
switch (action.type) {
case FETCH_TYPE:
// consume fetch, and print the resolution
action
.endpoint(...action.args)
.then(response => console.log(response));
default:
return next(action);
}
};
cleanup() {}
}
action.meta.args -> action.args
Os argumentos da action saíram de meta e foram para o nível superior. #3143
const { args, key } = action.meta;
const { args, key } = action;
Para autores de Schemas personalizados
Pule esta seção se você não tem schemas personalizados que implementam normalize() ou denormalize().
Mudanças na interface de Schema
As interfaces de normalize() e denormalize() mudaram. #3134
Isso traz um ganho de 10% no desempenho para todos os usuários.
Schema.normalize visit()
A interface de visit() agora remove os argumentos não contextuais:
/** Visits next data + schema while recurisvely normalizing */
export interface Visit {
(schema: any, value: any, parent: any, key: any, args: readonly any[]): any;
creating?: boolean;
}
processedEntity[key] = visit(
processedEntity[key],
processedEntity,
key,
this.schema[key],
addEntity,
visitedEntities,
storeEntities,
args,
);
processedEntity[key] = visit(
this.schema[key],
processedEntity[key],
processedEntity,
key,
args,
);
As informações necessárias desses argumentos são fornecidas ao fechar visit() em torno deles.
Interface de Schema.normalize
Mudou do acesso direto aos dados para o uso de funções como getEntity:
interface SchemaSimple {
normalize(
input: any,
parent: any,
key: any,
args: any[],
visit: (
schema: any,
value: any,
parent: any,
key: any,
args: readonly any[],
) => any,
addEntity: (...args: any) => any,
getEntity: (...args: any) => any,
checkLoop: (...args: any) => any,
): any;
}
Também adicionamos checkLoop(), que move parte da lógica de Entity
para o algoritmo central de normalização.
/** Returns true if a circular reference is found */
export interface CheckLoop {
(entityKey: string, pk: string, input: object): boolean;
}
Schema.denormalize unvisit()
O argumento unvisit agora recebe o argumento schema primeiro:
interface SchemaSimple {
denormalize(
input: {},
args: readonly any[],
unvisit: (schema: any, input: any) => any,
): T;
}
Suporte à atualização
Como sempre, se tiver qualquer problema ou dúvida, fique à vontade para entrar no nosso ou registrar um bug
