v0.14: APIs de Manager simplificadas, rendimiento y GC
Managers:
Las APIs de Manager se han simplificado: formas de acción más planas reducen la complejidad del código y mejoran el rendimiento al minimizar las entradas distintas de la caché inline, mientras que APIs de configuración más simples hacen que la personalización sea más agradable.
- Formas de acción simplificadas
- Propiedad Manager.middleware
- Configuración de getDefaultManagers()
Rendimiento:
- Llamadas fetch inmediatas: ya no se espera a que el sistema esté inactivo
- Bundles 30% más pequeños al eliminar polyfills innecesarios
- Normalización 10% más rápida, desnormalización 32% más rápida
Schema/Modelado de datos:
- Joins con Query usando Object Schemas
- EntityMixin para componer el comportamiento de Entity
- Valor predeterminado de Entity.pk() usa
id - Invalidación dinámica mediante Entity.process()
Pruebas:
- renderDataHook() para pruebas más simples
Recolección de basura:
- GCPolicy: recolección de basura automática de datos obsoletos
- NetworkManager obtiene los datos de inmediato
- action.payload -> action.response
- action.meta.args -> action.args
Managers
Las APIs de Manager se han simplificado: formas de acción más planas reducen la complejidad del código y mejoran el rendimiento al minimizar las entradas distintas de la caché inline, mientras que APIs de configuración más simples hacen que la personalización sea más agradable.
Formas de acción
Las formas de acción son más simples y consistentes. #3143, #3139
action.meta.args->action.argsaction.meta.key->action.keyaction.meta.nmeliminado (no se usaba)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
Los Managers ahora pueden usar una propiedad middleware en lugar del 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() sigue funcionando por compatibilidad hacia atrás.
actionTypes sin el sufijo _TYPE
Ya están disponibles nombres de tipos de acción más limpios (no es un cambio incompatible: también conservamos los nombres anteriores). #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() ahora acepta opciones de configuración. #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(),
});
Constructor de NetworkManager
El constructor de NetworkManager ahora usa argumentos con nombre para mayor claridad.
new NetworkManager(42, 7);
new NetworkManager({ dataExpiryLength: 42, errorExpiryLength: 7 });
Rendimiento
El modo concurrente y el batching automático de React 18+ cambian cómo debemos optimizar. Esperar a los callbacks de inactividad ya no es lo óptimo: obtener los datos de inmediato ahora da mejores resultados. Los cambios internos aprovechan closures y estructuras de datos Map para una normalización y búsqueda más rápidas, mientras que eliminar dependencias de polyfills infladas reduce significativamente los bundles.
Obtención inmediata
NetworkManager ahora obtiene los datos de inmediato en lugar de esperar a los callbacks de inactividad. #3146
Esto se traduce en una carga de datos más rápida, especialmente en aplicaciones con árboles de renderizado complejos.
IdlingNetworkManager sigue disponible para quienes prefieran el comportamiento anterior:
import { IdlingNetworkManager, getDefaultManagers } from '@data-client/react';
const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});
Tamaño del bundle
Los paquetes ahora son 30% más pequeños tras eliminar paquetes de polyfills inflados. #3384
Además, los polyfills ya no contaminan el ámbito global. #3353
Rendimiento de la normalización
Los cambios internos en la interfaz de schema aportan una mejora del 10% en el rendimiento de la normalización. #3134
Usar Map en lugar de objetos planos para almacenar entidades ofrece búsquedas hasta 32% más rápidas con conjuntos de datos grandes. #3390
Schema/Modelado de datos
Las APIs de schema reducen el código repetitivo en los patrones comunes y a la vez permiten casos de uso más avanzados. Los joins del lado del cliente ahora funcionan entre entidades arbitrarias (no solo relaciones anidadas), la invalidación dinámica cubre la lógica de eliminación que solo conoce el servidor, y valores predeterminados sensatos como Entity.pk() usando id eliminan código repetitivo.
Joins con Query
Query ahora puede recibir Object Schemas, lo que permite joins entre varios tipos de entidad. #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
Nuevo EntityMixin para componer el comportamiento de Entity con clases 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) {}
Valor predeterminado de Entity.pk()
Entity.pk() ahora devuelve this.id de forma predeterminada. #3188
class Todo extends Entity {
id = '';
title = '';
completed = false;
pk() {
return this.id;
}
}
class Todo extends Entity {
id = '';
title = '';
completed = false;
}
Invalidación dinámica
Devuelve undefined desde Entity.process() para invalidar entidades dinámicamente según los datos de la respuesta. #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() pasa a llamarse resource() por brevedad. #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 sigue exportándose por compatibilidad hacia atrás.
Pruebas
Probar debería ser simple. Los nuevos helpers usan valores predeterminados sensatos para eliminar el código repetitivo en los escenarios de prueba más comunes.
renderDataHook()
El nuevo renderDataHook() usa el DataProvider predeterminado, eliminando código repetitivo. #3238
Además, makeRenderDataClient() pasa a llamarse makeRenderDataHook() por consistencia (el nombre anterior sigue 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' },
},
],
},
);
Recolección de basura
Las aplicaciones de larga duración acumulan datos obsoletos en memoria. Las nuevas políticas de recolección de basura limpian automáticamente las entradas caducadas usando conteo de referencias para saber qué datos están activamente vinculados a componentes.
GCPolicy
GCPolicy habilita la recolección de basura automática de datos obsoletos. #3343
import { GCPolicy, DataProvider } from '@data-client/react';
// Run GC sweep every 10 minutes
<DataProvider gcPolicy={new GCPolicy({ intervalMS: 60 * 1000 * 10 })}>
{children}
</DataProvider>
Configura el comportamiento de caducidad:
new GCPolicy({
intervalMS: 60 * 1000 * 5, // How often to run GC
expiryMultiplier: 2, // How many stale lifetimes before GC
})
Usa ImmortalGCPolicy para desactivar la recolección de basura (el comportamiento predeterminado anterior):
import { ImmortalGCPolicy, DataProvider } from '@data-client/react';
<DataProvider gcPolicy={new ImmortalGCPolicy()}>
{children}
</DataProvider>
Otras mejoras
- Las Collections funcionan con schemas polimórficos como Union #3151
- Las Collections funcionan con argumentos anidados (compatibilidad con la biblioteca qs) #3281
- Los interceptors funcionan con acciones despachadas por managers #3365
- Los mensajes de NetworkError incluyen la URL para facilitar la depuración
- React Native:
InteractionManager.runAfterInteractionspara reducir la pérdida de frames #3127 - React Native: entrada
react-nativeen los exports de package.json #3371 - Mejor compatibilidad con React 19 #3279
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.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 los usuarios
NetworkManager obtiene los datos de inmediato
NetworkManager ahora obtiene los datos de inmediato en lugar de esperar a la inactividad. Esto mejora el rendimiento con React 18+. #3146
Para conservar el comportamiento anterior, usa IdlingNetworkManager:
import { IdlingNetworkManager, getDefaultManagers } from '@data-client/react';
const managers = getDefaultManagers({
networkManager: new IdlingNetworkManager(),
});
Para autores de Managers personalizados
Omite esta sección si no tienes Managers personalizados.
action.payload -> action.response
Para Managers personalizados que manejan 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 eliminado
Para Managers personalizados que manejan 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
Los argumentos de la acción se movieron de meta al nivel superior. #3143
const { args, key } = action.meta;
const { args, key } = action;
Para autores de Schemas personalizados
Omite esta sección si no tienes schemas personalizados que implementen normalize() o denormalize().
Cambios en la interfaz de Schema
Las interfaces normalize() y denormalize() han cambiado. #3134
Esto aporta una mejora de rendimiento del 10% para todos los usuarios.
visit() de Schema.normalize
La interfaz visit() ahora elimina los argumentos que no dependen del contexto:
/** 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,
);
La información que se necesitaba de esos argumentos se obtiene encerrando visit() sobre ellos en un closure.
Interfaz de Schema.normalize
Cambió del acceso directo a los datos al uso de funciones 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;
}
También agregamos checkLoop(), que traslada parte de la lógica de Entity
al algoritmo central de normalización.
/** Returns true if a circular reference is found */
export interface CheckLoop {
(entityKey: string, pk: string, input: object): boolean;
}
unvisit() de Schema.denormalize
El argumento unvisit ahora recibe primero el argumento schema:
interface SchemaSimple {
denormalize(
input: {},
args: readonly any[],
unvisit: (schema: any, input: any) => any,
): T;
}
Soporte para la actualización
Como siempre, si tienes algún problema o pregunta, no dudes en unirte a nuestro o reportar un error
