Saltar al contenido principal

v0.16: Obtención en paralelo, Collection.move, Lazy

· 14 min de lectura
Nathaniel Tucker
Creator of Reactive Data Client

Nuevas funcionalidades:

Otras mejoras:

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>
  );
}
Resultado

Cambios incompatibles:

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} />);
Resultado

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 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

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 v6Sintaxis v8Descripción
/:optional?{/:optional}Parámetro opcional
/:repeat+/*repeatRepetició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+)/:idSe eliminan las expresiones regulares personalizadas

Parámetros opcionales​

Se elimina el sufijo ?. Envuelve el segmento opcional (incluido su prefijo) en {}.

Before
const ep = new RestEndpoint({
path: '/:group/things/:number?',
});
After
const ep = new RestEndpoint({
path: '/:group/things{/:number}',
});

Varios segmentos opcionales con prefijos distintos:

Before
const ep = new RestEndpoint({
path: '/:attr1?{-:attr2}?{-:attr3}?',
});
After
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.

Before
const ep = new RestEndpoint({
path: '/files/:path+',
});
ep({ path: 'docs/reports/q4' });
const optEp = new RestEndpoint({
path: '/files/:path*',
});
After
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:

Before
const ep = new RestEndpoint({
path: '/:with-dash',
});
After
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.

Before
const ep = new RestEndpoint({
path: '/users/:id(\\d+)',
});
After
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:

Before
const ep = new RestEndpoint({
path: '/search\\?q=:q',
});
After
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:

npx skills add reactive/data-client --skill path-to-regexp-v8-migration

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:

Before
const promise = useFetch(PostResource.get, { id });
if (promise) {
// fetch was triggered
}
After
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.

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

const myUnion = new schema.Union(
{ users: User, groups: Group },
'type',
);
After
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 Chat o reportar un error