v0.16: Busca em paralelo, Collection.move, Lazy
Novos recursos:
- Carregamento de dados em paralelo com useFetch() - Busque vários endpoints de forma concorrente com
use(useFetch()), evitando cascatas sequenciais - Collection.move - Mova entidades entre Collections com uma única operação
- Collection.moveWith() - Personalize o comportamento de mover (por exemplo, inserir no início em vez de no final)
- Lazy - Desnormalização adiada de relações, para desempenho e isolamento de memoização
Outras melhorias:
- Imports diretos de schema - Importe as classes de schema diretamente, sem o namespace
schema - Limite de profundidade de desnormalização - Evita estouro de pilha em grandes grafos bidirecionais de entidades; configurável via
Entity.maxEntityDepth(#3822) - DevToolsManager expõe
globalThis.__DC_CONTROLLERS__em modo de desenvolvimento para acesso programático à store a partir do Chrome DevTools MCP e do Expo MCP. Use a skill data-client-react para habilitar a depuração assistida por IA. - Remoção do aviso enganoso 'Uncaught Suspense' durante o SSR do Next.js
- Correção do tipo
sideEffect: falseque se perdia commethod: 'POST'em RestEndpoint - renderDataHook() faz limpeza automática após cada teste — não são mais necessárias chamadas manuais a
cleanup() - renderDataHook() retorna
cleanup()eallSettled()por renderização, para um encerramento confiável dos testes
Collection.move facilita mover entidades entre Collections com uma única operação:
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 - Sintaxe de RestEndpoint.path atualizada:
/:optional?→{/:optional},/:repeat+→/*repeat(tipado comostring[]) - useFetch() retorna UsablePromise - Retorna um thenable para
React.use(); verifique.resolvedem vez da truthiness
Atualize com um codemod automatizado para todas as mudanças incompatíveis:
npx jscodeshift -t https://dataclient.io/codemods/v0.16.js --extensions=ts,tsx,js,jsx src/
Collection.move
Collection.move e RestEndpoint.move facilitam mover entidades entre collections com uma única requisição PATCH. A entidade é removida automaticamente das collections que correspondem ao seu estado antigo e adicionada às collections que correspondem aos novos valores do corpo da requisição.
Isso é útil para quadros kanban, reatribuição de grupos, mudanças de status ou qualquer fluxo em que um item se move 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 com parâmetros de caminho quanto com parâmetros de busca, e suporta collections de Array e Values.
Collection.moveWith()
Collection.moveWith() constrói um schema de mover personalizado, análogo a addWith(). A função merge controla como as entidades são adicionadas à collection de destino (por exemplo, inserindo no início em vez de no final), enquanto o comportamento de remoção é derivado automaticamente. A função de merge unshift é fornecida por conveniência.
import { Collection, unshift } from '@data-client/rest';
class MyCollection extends Collection {
constructor(schema, options) {
super(schema, options);
this.move = this.moveWith(unshift);
}
}
Carregamento de dados em paralelo
useFetch() agora retorna um thenable UsablePromise,
tornando use(useFetch(endpoint, args)) um substituto direto de useSuspense() —
ele suspende enquanto os dados carregam, retorna os dados desnormalizados quando estão em cache, lança em caso de erro
e suspende novamente após uma invalidação.
Como useFetch() e use() são chamadas separadas, várias buscas começam em paralelo.
Todas as chamadas a useFetch() executam antes que qualquer use() suspenda, então toda busca já está em andamento
mesmo que o primeiro use() suspenda o componente.
Isso evita a cascata sequencial que ocorre com várias chamadas a useSuspense(),
em que a segunda busca não pode começar até que a primeira seja resolvida.
#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} />);
Limite de profundidade de desnormalização e Lazy
Quando os schemas têm relações bidirecionais entre tipos de entidade
(por exemplo, Department → Building → Department → ...), a desnormalização percorre cada
entidade única do grafo conectado. Com conjuntos de dados pequenos isso passa despercebido, mas em escala — milhares
de entidades interconectadas — a recursão excede a pilha de chamadas do JS e lança
RangeError: Maximum call stack size exceeded.
#3822
Dois recursos complementares resolvem isso: um limite de profundidade como rede de segurança e Lazy para controle preciso, campo a campo, de quais relações são resolvidas de forma antecipada.
Limite de profundidade de desnormalização
A desnormalização agora impõe um limite de profundidade padrão de 64 saltos entre entidades. Entidades além
do limite são retornadas com ids não resolvidos em vez de objetos totalmente desnormalizados.
Um console.error é emitido em modo de desenvolvimento quando o limite é atingido.
O limite pode ser configurado por Entity com static maxEntityDepth:
class Department extends Entity {
static maxEntityDepth = 16;
}
Isso evita travamentos, mas é um instrumento grosseiro — corta todos os caminhos profundos, inclusive os legítimos e não cíclicos. Para um controle refinado, use Lazy.
Lazy
Lazy envolve um schema para pular a desnormalização antecipada de campos de relação específicos.
Durante a desnormalização da entidade pai, o campo mantém seu valor normalizado bruto (chaves primárias).
A relação pode então ser resolvida sob demanda via useQuery usando o acessor .query.
import { Entity, Lazy } from '@data-client/rest';
class Department extends Entity {
id = '';
name = '';
buildings: string[] = [];
static schema = {
buildings: new Lazy([Building]),
};
}
Diferente do limite de profundidade, Lazy é uma adesão direcionada, por relação. Apenas os
campos que você marca como lazy pulam a resolução antecipada — o resto do schema é desnormalizado normalmente.
Isso também melhora o desempenho ao adiar o trabalho em relações que nem sempre são
necessárias, e oferece isolamento de memoização — mudanças em entidades lazy não invalidam
a forma desnormalizada do pai.
Veja a documentação de Lazy para todos os detalhes de uso e #3828 para a discussão de design.
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.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>
Um codemod automatizado trata das três mudanças incompatíveis abaixo — sintaxe de caminho, truthiness de useFetch() e imports do namespace schema:
npx jscodeshift -t https://dataclient.io/codemods/v0.16.js --extensions=ts,tsx,js,jsx src/
path-to-regexp v8
RestEndpoint.path agora usa o path-to-regexp v8, que muda a sintaxe de parâmetros opcionais, parâmetros repetidos e caracteres especiais.
| Sintaxe v6 | Sintaxe v8 | Descrição |
|---|---|---|
/:optional? | {/:optional} | Parâmetro opcional |
/:repeat+ | /*repeat | Repetição de um ou mais (tipado como string[]) |
/:repeat* | {/*repeat} | Repetição de zero ou mais (tipado como string[]) |
/:with-dash | /:"with-dash" | Nome de parâmetro entre aspas |
/:id(\d+) | /:id | Regex personalizada removida |
Parâmetros opcionais
O sufixo ? foi removido. Envolva o segmento opcional (incluindo seu prefixo) em {}.
const ep = new RestEndpoint({
path: '/:group/things/:number?',
});
const ep = new RestEndpoint({
path: '/:group/things{/:number}',
});
Vários segmentos opcionais com prefixos diferentes:
const ep = new RestEndpoint({
path: '/:attr1?{-:attr2}?{-:attr3}?',
});
const ep = new RestEndpoint({
path: '{/:attr1}{-:attr2}{-:attr3}',
});
Parâmetros repetidos
Os sufixos + (um ou mais) e * (zero ou mais) foram removidos. Use a sintaxe de curinga *name, envolvida em {} para zero ou mais.
Parâmetros curinga agora são tipados como string[] (arrays) em vez de string | number, já que representam vários segmentos de caminho.
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}',
});
Nomes de parâmetros entre aspas
Nomes de parâmetros com caracteres especiais (hifens, pontos) agora precisam estar entre aspas duplas:
const ep = new RestEndpoint({
path: '/:with-dash',
});
const ep = new RestEndpoint({
path: '/:"with-dash"',
});
Regex personalizada removida
Padrões de regex inline como /:id(\d+) não são mais suportados. Remova a regex — valide no servidor em vez disso.
const ep = new RestEndpoint({
path: '/users/:id(\\d+)',
});
const ep = new RestEndpoint({
path: '/users/:id',
});
Escapando caracteres especiais
Caracteres como {}()[]+?!:*\\ precisam ser escapados com \\ quando usados como literais no padrão de caminho.
Esses caracteres são reservados pelo path-to-regexp e
seu uso sem escape lança um erro Unexpected token.
Uma query string literal no padrão ainda precisa de um ? escapado, por exemplo '/resource\\?per_page=50'.
Quando os segmentos de query usam parâmetros nomeados, prefira grupos explícitos em vez de um único ? escapado:
const ep = new RestEndpoint({
path: '/search\\?q=:q',
});
const ep = new RestEndpoint({
path: '/search?{q=:q}{&page=:page}',
});
Encontrando caminhos para migrar
Procure estes padrões nos valores de path: do seu código:
?depois de um:param— parâmetros opcionais a converter+ou*depois de um:param— parâmetros repetidos a converter(\depois de um:param— regex personalizada a remover- Nomes de parâmetros com
-ou.— precisam de aspas
Veja RestEndpoint.path para a documentação completa da sintaxe v8, ou os erros do path-to-regexp para explicações detalhadas. A migração assistida por IA também está disponível:
- 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() retorna UsablePromise
useFetch() agora retorna um thenable UsablePromise com dados desnormalizados, tratamento de erros e uma propriedade .resolved. Isso o torna diretamente compatível com React.use() — use(useFetch(endpoint, args)) se comporta de forma idêntica a useSuspense().
Antes, useFetch() retornava undefined quando os dados eram válidos. Código que verificava se houve uma busca pela truthiness agora precisa 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
Veja Carregamento de dados em paralelo acima para saber como isso habilita buscas concorrentes que evitam cascatas sequenciais. #3752, #3755
Imports diretos de schema
As classes de schema agora podem ser importadas diretamente de @data-client/endpoint (ou de @data-client/rest, que as reexporta), sem exigir o namespace schema. Isso oferece uma sintaxe de import mais limpa, mantendo total compatibilidade retroativa.
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 as classes de schema estão disponíveis como exports diretos: Union, Invalidate, Collection, Query, Values, All e Lazy. O export do namespace schema continua disponível por compatibilidade retroativa.
Suporte à atualização
Como sempre, se tiver qualquer problema ou dúvida, fique à vontade para entrar no nosso ou registrar um bug
