v0.16: Obtención en paralelo, Collection.move, Lazy
Nuevas funcionalidades:
- Carga de datos en paralelo con useFetch(): obtén varios endpoints de forma concurrente con
use(useFetch()), evitando cascadas secuenciales - Collection.move: mueve entidades entre Collections con una sola operación
- Collection.moveWith(): personaliza el comportamiento del movimiento (p. ej., insertar al inicio en lugar de al final)
- Lazy: desnormalización diferida de relaciones para mejorar el rendimiento y aislar la memoización
Otras mejoras:
- Importaciones directas de schemas: importa las clases de schema directamente sin el espacio de nombres
schema - Límite de profundidad de desnormalización: evita el desborde de la pila en grafos de entidades bidireccionales grandes; configurable mediante
Entity.maxEntityDepth(#3822) - DevToolsManager expone
globalThis.__DC_CONTROLLERS__en modo de desarrollo para acceder al store de forma programática desde Chrome DevTools MCP y Expo MCP. Usa el skill data-client-react para habilitar la depuración asistida por IA. - Se elimina la advertencia engañosa 'Uncaught Suspense' durante el SSR de Next.js
- Se corrige la pérdida del tipo
sideEffect: falseal usarmethod: 'POST'en RestEndpoint - renderDataHook() limpia automáticamente después de cada prueba: ya no hacen falta llamadas manuales a
cleanup() - renderDataHook() devuelve
cleanup()yallSettled()por cada renderizado para un desmontaje confiable de las pruebas
Collection.move facilita mover entidades entre Collections con una sola operación:
import { useController } from '@data-client/react'; import { TaskResource, type Task } from './TaskResource'; export default function TaskCard({ task }: { task: Task }) { const handleMove = () => ctrl.fetch( TaskResource.getList.move, { id: task.id }, { id: task.id, status: task.status === 'backlog' ? 'in-progress' : 'backlog' }, ); const ctrl = useController(); return ( <div className="listItem"> <span style={{ flex: 1 }}>{task.title}</span> <button onClick={handleMove}> {task.status === 'backlog' ? '\u25bc' : '\u25b2'} </button> </div> ); }
- path-to-regexp v8: la sintaxis de RestEndpoint.path se actualizó:
/:optional?→{/:optional},/:repeat+→/*repeat(tipado comostring[]) - useFetch() devuelve UsablePromise: devuelve un thenable para
React.use(); comprueba.resolveden lugar de su veracidad
Actualiza con un codemod automatizado para todos los cambios incompatibles:
npx jscodeshift -t https://dataclient.io/codemods/v0.16.js --extensions=ts,tsx,js,jsx src/
Collection.move
Collection.move y RestEndpoint.move facilitan mover entidades entre collections con una sola solicitud PATCH. La entidad se elimina automáticamente de las collections que coinciden con su estado anterior y se agrega a las collections que coinciden con los nuevos valores del cuerpo de la solicitud.
Esto es útil para tableros kanban, reasignación de grupos, cambios de estado o cualquier flujo de trabajo en el que un elemento se mueva entre listas filtradas.
const TaskResource = resource({
path: '/tasks/:id',
searchParams: {} as { status: string },
schema: Task,
});
// PATCH /tasks/3 - moves task from 'backlog' to 'in-progress'
await ctrl.fetch(
TaskResource.getList.move,
{ id: '3' },
{ id: '3', status: 'in-progress' },
);
Funciona tanto con parámetros de ruta como con parámetros de búsqueda, y admite collections de tipo Array y Values.
Collection.moveWith()
Collection.moveWith() construye un schema de movimiento personalizado, análogo a addWith(). La función merge controla cómo se agregan las entidades a su collection de destino (p. ej., insertando al inicio en lugar de al final), mientras que el comportamiento de eliminación se deriva automáticamente. Por comodidad se incluye la función de merge unshift.
import { Collection, unshift } from '@data-client/rest';
class MyCollection extends Collection {
constructor(schema, options) {
super(schema, options);
this.move = this.moveWith(unshift);
}
}
Carga de datos en paralelo
useFetch() ahora devuelve un thenable UsablePromise,
lo que convierte a use(useFetch(endpoint, args)) en un equivalente directo de useSuspense():
suspende mientras se cargan los datos, devuelve los datos desnormalizados cuando están en caché, lanza en caso de error
y vuelve a suspender tras una invalidación.
Como useFetch() y use() son llamadas separadas, varias obtenciones se inician en paralelo.
Todas las llamadas a useFetch() se ejecutan antes de que cualquier use() suspenda, así que cada obtención ya está en curso
incluso si el primer use() suspende el componente.
Esto evita la cascada secuencial que ocurre con varias llamadas a useSuspense(),
donde la segunda obtención no puede comenzar hasta que se resuelve la primera.
#3755
import { use } from 'react'; import { useFetch } from '@data-client/react'; import { PostResource, CommentResource } from './Resources'; function PostWithComments({ id }: { id: number }) { // Both fetches start in parallel const postPromise = useFetch(PostResource.get, { id }); const commentsPromise = useFetch(CommentResource.getList, { postId: id }); // use() reads the results — if the first suspends, // the second fetch is already in-flight const post = use(postPromise); const comments = use(commentsPromise); return ( <article> <h3>{post.title}</h3> <p>{post.body}</p> <h4>Comments</h4> {comments.map(comment => ( <div key={comment.id} className="listItem"> <strong>{comment.author}</strong>: {comment.text} </div> ))} </article> ); } render(<PostWithComments id={1} />);
Límite de profundidad de desnormalización y Lazy
Cuando los schemas tienen relaciones bidireccionales entre tipos de entidad
(p. ej., Department → Building → Department → ...), la desnormalización recorre cada
entidad única del grafo conectado. Con conjuntos de datos pequeños esto pasa desapercibido, pero a gran escala (miles
de entidades interconectadas) la recursión excede la pila de llamadas de JS y lanza
RangeError: Maximum call stack size exceeded.
#3822
Dos funcionalidades complementarias abordan este problema: un límite de profundidad como red de seguridad, y Lazy para un control preciso, campo por campo, de qué relaciones se resuelven de forma anticipada.
Límite de profundidad de desnormalización
La desnormalización ahora aplica un límite de profundidad predeterminado de 64 saltos entre entidades. Las entidades más allá
del límite se devuelven con sus ids sin resolver en lugar de objetos completamente desnormalizados.
Se emite un console.error en modo de desarrollo cuando se alcanza el límite.
El límite puede configurarse por Entity con static maxEntityDepth:
class Department extends Entity {
static maxEntityDepth = 16;
}
Esto evita los fallos, pero es una herramienta poco precisa: corta todas las rutas profundas, incluidas las legítimas que no son cíclicas. Para un control más fino, usa Lazy.
Lazy
Lazy envuelve un schema para omitir la desnormalización anticipada de campos de relación específicos.
Durante la desnormalización de la entidad padre, el campo conserva su valor normalizado sin procesar (las claves primarias).
La relación puede resolverse después bajo demanda mediante useQuery usando el accesor .query.
import { Entity, Lazy } from '@data-client/rest';
class Department extends Entity {
id = '';
name = '';
buildings: string[] = [];
static schema = {
buildings: new Lazy([Building]),
};
}
A diferencia del límite de profundidad, Lazy es una opción selectiva por relación. Solo los
campos que marques como lazy omiten la resolución anticipada; el resto del schema se desnormaliza con normalidad.
Esto también mejora el rendimiento al diferir el trabajo de las relaciones que no siempre se
necesitan, y proporciona aislamiento de la memoización: los cambios en las entidades lazy no invalidan
la forma desnormalizada del padre.
Consulta la documentación de Lazy para ver todos los detalles de uso y #3828 para la discusión de diseño.
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.16.0 @data-client/rest@^0.16.0 @data-client/endpoint@^0.16.0 @data-client/core@^0.16.0 @data-client/vue@^0.16.0 @data-client/test@^0.16.0 @data-client/img@^0.16.0
yarn add @data-client/react@^0.16.0 @data-client/rest@^0.16.0 @data-client/endpoint@^0.16.0 @data-client/core@^0.16.0 @data-client/vue@^0.16.0 @data-client/test@^0.16.0 @data-client/img@^0.16.0
pnpm add @data-client/react@^0.16.0 @data-client/rest@^0.16.0 @data-client/endpoint@^0.16.0 @data-client/core@^0.16.0 @data-client/vue@^0.16.0 @data-client/test@^0.16.0 @data-client/img@^0.16.0
<script type="importmap">
{
"imports": {
"@data-client/react": "https://esm.sh/@data-client/react@^0.16.0",
"@data-client/rest": "https://esm.sh/@data-client/rest@^0.16.0",
"@data-client/endpoint": "https://esm.sh/@data-client/endpoint@^0.16.0",
"@data-client/core": "https://esm.sh/@data-client/core@^0.16.0",
"@data-client/vue": "https://esm.sh/@data-client/vue@^0.16.0",
"@data-client/test": "https://esm.sh/@data-client/test@^0.16.0",
"@data-client/img": "https://esm.sh/@data-client/img@^0.16.0"
}
}
</script>
Un codemod automatizado se encarga de los tres cambios incompatibles que se describen a continuación: la sintaxis de rutas, la veracidad de useFetch() y las importaciones del espacio de nombres schema:
npx jscodeshift -t https://dataclient.io/codemods/v0.16.js --extensions=ts,tsx,js,jsx src/
path-to-regexp v8
RestEndpoint.path ahora usa path-to-regexp v8, que cambia la sintaxis de los parámetros opcionales, los parámetros repetidos y los caracteres especiales.
| Sintaxis v6 | Sintaxis v8 | Descripción |
|---|---|---|
/:optional? | {/:optional} | Parámetro opcional |
/:repeat+ | /*repeat | Repetición de uno o más (tipado como string[]) |
/:repeat* | {/*repeat} | Repetición de cero o más (tipado como string[]) |
/:with-dash | /:"with-dash" | Nombre de parámetro entre comillas |
/:id(\d+) | /:id | Se eliminan las expresiones regulares personalizadas |
Parámetros opcionales
Se elimina el sufijo ?. Envuelve el segmento opcional (incluido su prefijo) en {}.
const ep = new RestEndpoint({
path: '/:group/things/:number?',
});
const ep = new RestEndpoint({
path: '/:group/things{/:number}',
});
Varios segmentos opcionales con prefijos distintos:
const ep = new RestEndpoint({
path: '/:attr1?{-:attr2}?{-:attr3}?',
});
const ep = new RestEndpoint({
path: '{/:attr1}{-:attr2}{-:attr3}',
});
Parámetros repetidos
Se eliminan los sufijos + (uno o más) y * (cero o más). Usa la sintaxis de comodín *name, envuelta en {} para cero o más.
Los parámetros comodín ahora se tipan como string[] (arreglos) en lugar de string | number, ya que representan varios segmentos de la ruta.
const ep = new RestEndpoint({
path: '/files/:path+',
});
ep({ path: 'docs/reports/q4' });
const optEp = new RestEndpoint({
path: '/files/:path*',
});
const ep = new RestEndpoint({
path: '/files/*path',
});
ep({ path: ['docs', 'reports', 'q4'] });
const optEp = new RestEndpoint({
path: '/files{/*path}',
});
Nombres de parámetros entre comillas
Los nombres de parámetros con caracteres especiales (guiones, puntos) ahora deben ir entre comillas dobles:
const ep = new RestEndpoint({
path: '/:with-dash',
});
const ep = new RestEndpoint({
path: '/:"with-dash"',
});
Se eliminan las expresiones regulares personalizadas
Los patrones de expresiones regulares inline como /:id(\d+) ya no se admiten. Elimina la expresión regular y valida en el servidor en su lugar.
const ep = new RestEndpoint({
path: '/users/:id(\\d+)',
});
const ep = new RestEndpoint({
path: '/users/:id',
});
Escapar caracteres especiales
Los caracteres como {}()[]+?!:*\\ deben escaparse con \\ cuando se usan como literales en el patrón de la ruta.
Estos caracteres están reservados por path-to-regexp y
usarlos sin escapar lanza un error Unexpected token.
Una cadena de consulta literal en el patrón sigue necesitando un ? escapado, p. ej. '/resource\\?per_page=50'.
Cuando los segmentos de consulta usan parámetros con nombre, prefiere grupos explícitos en lugar de un único ? escapado:
const ep = new RestEndpoint({
path: '/search\\?q=:q',
});
const ep = new RestEndpoint({
path: '/search?{q=:q}{&page=:page}',
});
Encontrar las rutas que hay que migrar
Busca en tu base de código estos patrones en los valores de path::
?después de un:param: parámetros opcionales que hay que convertir+o*después de un:param: parámetros repetidos que hay que convertir(\después de un:param: expresiones regulares personalizadas que hay que eliminar- Nombres de parámetros con
-o.: necesitan comillas
Consulta RestEndpoint.path para la documentación completa de la sintaxis v8, o los errores de path-to-regexp para explicaciones detalladas. También está disponible la migración asistida por IA:
- Skills
- OpenSkills
- Claude Code
npx skills add reactive/data-client --skill path-to-regexp-v8-migration
npx openskills install reactive/data-client/.agents/skills/path-to-regexp-v8-migration
claude plugin marketplace add reactive/data-client
claude plugin install migrations@data-client
useFetch() devuelve UsablePromise
useFetch() ahora devuelve un thenable UsablePromise con datos desnormalizados, manejo de errores y una propiedad .resolved. Esto lo hace directamente compatible con React.use(): use(useFetch(endpoint, args)) se comporta exactamente igual que useSuspense().
Antes, useFetch() devolvía undefined cuando los datos eran válidos. El código que comprobaba si hubo una obtención mediante su veracidad ahora debe usar .resolved:
const promise = useFetch(PostResource.get, { id });
if (promise) {
// fetch was triggered
}
const promise = useFetch(PostResource.get, { id });
if (!promise.resolved) {
// fetch is in-flight
}
use(promise); // suspends or returns denormalized data
Consulta Carga de datos en paralelo más arriba para ver cómo esto permite obtenciones concurrentes que evitan las cascadas secuenciales. #3752, #3755
Importaciones directas de schemas
Las clases de schema ahora pueden importarse directamente desde @data-client/endpoint (o desde @data-client/rest, que las reexporta) en lugar de requerir el espacio de nombres schema. Esto ofrece una sintaxis de importación más limpia manteniendo la compatibilidad hacia atrás completa.
import { schema } from '@data-client/endpoint';
const myUnion = new schema.Union(
{ users: User, groups: Group },
'type',
);
import { Union } from '@data-client/endpoint';
const myUnion = new Union(
{ users: User, groups: Group },
'type',
);
Todas las clases de schema están disponibles como exportaciones directas: Union, Invalidate, Collection, Query, Values, All y Lazy. La exportación del espacio de nombres schema sigue disponible por compatibilidad hacia atrás.
Soporte para la actualización
Como siempre, si tienes algún problema o pregunta, no dudes en unirte a nuestro o reportar un error
