Pular para o conteúdo principal

v0.14: APIs de Manager simplificadas, desempenho e GC

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

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.

Desempenho:

Schema/modelagem de dados:

Testes:

Coleta de lixo:

  • GCPolicy - coleta de lixo automática de dados desatualizados

Mudanças incompatíveis:

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.args
  • action.meta.key -> action.key
  • action.meta.nm removido (não era usado)
  • action.payload -> action.response (para SET_RESPONSE)
Before
{
"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
}
After
{
"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

Before
class MyManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
// handle action
return next(action);
};

cleanup() {}
}
After
class MyManager implements Manager {
middleware: Middleware = controller => next => async action => {
// handle action
return next(action);
};

cleanup() {}
}
dica

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

Before
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() {}
}
After
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.

Before
new NetworkManager(42, 7);
After
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

Benchmarks ao longo do tempo | Ver benchmark

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

Before
class Todo extends Entity {
id = '';
title = '';
completed = false;

pk() {
return this.id;
}
}
After
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

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

const TodoResource = createResource({
path: '/todos/:id',
schema: Todo,
});
After
import { resource } from '@data-client/rest';

const TodoResource = resource({
path: '/todos/:id',
schema: Todo,
});
observação

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.runAfterInteractions para reduzir quedas de frames #3127
  • React Native: entrada react-native nos 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 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

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

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

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

Before
const { args, key } = action.meta;
After
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;
}
Before
processedEntity[key] = visit(
processedEntity[key],
processedEntity,
key,
this.schema[key],
addEntity,
visitedEntities,
storeEntities,
args,
);
After
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 Chat ou registrar um bug