Pular para o conteúdo principal

v0.19: Coding Agents, Batch Controller.set(), Faster TypeScript

· 37 min para ler
Nathaniel Tucker
Criador do Reactive Data Client

v0.19 makes coding agents a first-class way to build with Data Client: one prompt sets up your app, and your agent follows skills that ship with the packages you installed. It also lets a Manager write a whole batch of streamed rows in one store update, type-checks endpoint code about 2x faster, and fixes a round of TypeScript and Vue issues.

Coding Agents:

  • One-prompt setup - Add the data-client plugin to Claude Code, Cursor or Codex and ask it to set up Data Client; it installs the packages, adds the provider and links the skills your app needs
  • Skills ship in the packages - npx --no data-client-skills links the skills your installed @data-client/* packages carry, so your agent follows the version you have instead of an older release
  • Agents keep using Data Client - Setup adds a note to AGENTS.md, and each skill names the everyday tasks it covers, so later sessions reach for Data Client instead of fetch in useEffect
  • Debug the running app - In development, globalThis.__DC__ gives your agent the store's Controller and recent actions from Cursor's browser, Chrome DevTools MCP, Playwright MCP or Claude in Chrome
  • Vue skills, llms.txt, and skills generated from these docs

New APIs:

Performance:

Other Improvements:

  • Fix TypeScript 7 module resolution for package exports; imports now resolve to declaration files (#4019)
  • renderDataHook() runs provider mount effects when the first render suspends (#4099)
  • Controller.set() values are typed by the schema, so ctrl.set(new schema.All(Todo), 42) is a TypeScript error (#4133)
  • Controller.set() deletes one entity with an Invalidate schema without the one-row batch workaround (#4230)
  • process() gets typed params in new RestEndpoint(), .extend() and resource().extend(), so a wrong path parameter is a TypeScript error instead of undefined at runtime (#4183, #4184)
  • getOptimisticResponse() returns are typed by the schema, so a wrong field type is a TypeScript error instead of a flash of bad data (#4198)
  • Vue useSuspense() and useLive() keep the previous data while new arguments load, instead of returning undefined (#4131)
  • Vue useSuspense() and useLive() send fetch errors after arguments change to onErrorCaptured() instead of an unhandled promise rejection (#4135)
  • Vue useSuspense(), useDLE() and useFetch() no longer refetch stale data on every store update, so a controller.set() is not overwritten (#4134)
  • Vue useSuspense() shows stale data on mount and refetches in the background, like React, instead of showing the <Suspense> fallback until the refetch finishes (#4169)
  • Vue useDLE() and useCache() keep expired invalidIfStale data through unrelated store updates instead of getting stuck loading (#4142)
  • Vue DataClientPlugin installs on Vue versions before 3.5 instead of throwing app.onUnmount is not a function (#4146)
  • Vue DataClientPlugin garbage collects by default, matching React's DataProvider, so you no longer need to pass gcPolicy; how it decides what to remove (#4148)
  • Vue composables accept getter arguments like () => ({ id: props.id }); they were typed to allow them but passed the function itself to the endpoint (#4115)
  • Fix Cannot find name 'NoInfer' and export type errors on TypeScript 4.x with skipLibCheck off (#4138)
  • Fix Entity, Endpoint, Union and RestEndpoint type errors on TypeScript 4.0–4.5 with skipLibCheck off (#4140)
  • Fix ',' expected errors importing @data-client/react/redux, @data-client/react/nextjs or @data-client/test on TypeScript 4.0–4.4 (#4232)
  • Fix act and renderHook from @data-client/test typed as any on TypeScript 4.0–4.4 (#4251)
  • React Native useSuspense(), useFetch() and useDLE() refetch on screen focus only when the current data is stale or invalid, instead of refetching fresh data on every refocus (#4378)
  • renderDataHook() works with React Native Testing Library 14 and stays synchronous, so renderDataHook() calls need no await (#4374)
  • Entity classes can be used where an EntityInterface is expected (#4149); prepare pk() overrides for a future release
  • useCache() and useDLE() return undefined for a deleted entity whose refetch failed, instead of a truthy Symbol that slipped past if (!data) checks; remove any workarounds (#4150)
  • Vue useFetch() keeps its data from being garbage collected while mounted, like useSuspense(), so a configured gcPolicy no longer evicts prefetched data that components read later (#4152)
  • Apps with Redux DevTools open no longer stutter in development on large stores or frequent updates; each update serializes 40-60x faster (#4163)
  • resource().extend() with an object of endpoint overrides keeps endpoints added earlier with .extend('name', options) in its type, so using them is no longer a TypeScript error (#4184)
  • RestEndpoint.push, unshift and remove accept the Entity's fields as the body when a Query or Lazy wraps the Collection, instead of only FormData (#4226)
  • Vue useFetch() is typed as the read-only Ref it returns, so promise.resolved (always undefined) is now a TypeScript error; read promise.value.resolved instead (#4114)
  • A Controller subclass passed to DataProvider or DataClientPlugin no longer needs a cast to type-check (#4228)
  • prepareStore()'s store can be passed to react-redux's <Provider> without a cast, and has dispatch() in its type (#4238)
  • resource().create accepts FormData without a cast, like update, partialUpdate and getList.push (#4235)
  • RestEndpoint.push, unshift and remove send an array body as JSON, so creating or removing several items at once no longer sends "[object Object]" to your server; resource().create takes an array too (#4262)
  • Polling keeps the schema of endpoints given pollFrequency with .extend(), so a polled useLive() list updates right away from mutations and optimistic updates instead of waiting for the next poll (#4437)
  • DataProvider only warns that it doesn't update during SSR when something fetches while the server renders, instead of on every server render, so pages that load data after mount (like Expo Router's static web output) stay quiet (#4449)
  • Server rendering with React 19 no longer logs "Uncaught suspense" when nothing suspended; React renders and discards fallbacks of large boundaries, and the warning now waits for a fallback that actually shows (#4450)
  • Vue createDataClient() is deprecated, since it's internal to DataClientPlugin; install the plugin with app.use(DataClientPlugin, options) instead of calling it directly (#4193)
  • Vue renderDataCompose()'s waitForNextUpdate() is deprecated, since it gives up silently after 1 second and lets a test pass while still suspended; await result instead, or await allSettled() after a change, which now also waits for fetches a prop change starts (unit testing composables, #4221)

Breaking Changes:

Get ready for future releases:

Coding agents​

Coding agents write a lot of Data Client code, and they write it well when they have the current APIs instead of guessing from older releases they were trained on. v0.19 gets your agent those APIs in one step, keeps them matched to the version you install, and lets it check the running app.

Set up with one prompt​

Add the data-client plugin to your agent, then ask it to set up Data Client:

Terminal
claude plugin marketplace add reactive/data-client --scope project \
--sparse .claude-plugin .agents/skills
claude plugin install data-client@data-client --scope project
Execute no seu terminal
Copiar o prompt para o Claude Code

The setup skill detects React or Vue and your API style (REST, GraphQL or custom async functions), installs the packages, adds the provider, and links the skills those packages ship. For REST it also moves your existing fetch or axios calls to endpoints (#4153). Claude Code installs the plugin for the project, so teammates who open it get the plugin too. The package READMEs on npm open with the same setup (#4454).

The plugin only sets apps up, so it carries just the setup skill and the data-client skill below. Every other skill comes from your packages, below. Agents without a plugin take the Other agents prompt, which installs the setup skill with the skills CLI first. #4410, #4425, #4427, #4439, #4440

Skills ship in the packages​

Each @data-client/* package now carries the skills for its own APIs, so your agent follows the version you have installed and gets new guidance with each upgrade. data-client-skills (in @data-client/react, vue, rest, graphql and endpoint) links them into .agents/skills, with .claude/skills linking to it, where Claude Code, Cursor and Codex load them as their own skills.

Skills used to be copied from GitHub master; now they come from the packages you installed.
Before
npx skills add reactive/data-client \
--skill data-client-react --skill data-client-rest --skill data-client-schema
Execute no seu terminal
After
npx --no data-client-skills
Execute no seu terminal

Commit the links, and run the command again after adding, removing or upgrading @data-client packages (Agent Skills has the details). If you installed skills with npx skills add before, remove those copies first (npx skills remove data-client-react -y); the command keeps folders it didn't create and says so. #4407, #4410

Upgrade skills ship in the packages too. data-client-v0.18-migration runs the codemod, then handles what it can't (v0.18 migration), and path-to-regexp-v8-migration rewrites path syntax in your endpoints.

Agents keep using Data Client​

An agent in a later session doesn't know the setup happened, so it can fall back to fetch in useEffect or app-wide context. Setup now adds a note to your project's AGENTS.md, which Cursor, Codex and Claude Code read at the start of every session (setup adds @AGENTS.md to your CLAUDE.md if you have one):

AGENTS.md
This app manages server state (fetching, caching, mutations and real-time updates) with Reactive Data Client. Use it
for all server data, and read the matching `data-client-*` skill before writing, changing or debugging code that loads
or changes data. After adding, removing or upgrading `@data-client/*` packages, run `npx --no data-client-skills` and
commit the skill links.

Each skill's description now names the work it covers in everyday terms, like loading and error states, forms, or data that looks stale on screen, instead of listing hook names, so agents load it for those tasks even when you never mention Data Client. Adding or changing an endpoint that returns data loads data-client-schema, so the endpoint's responses are normalized into the shared cache with Entities. The data-client skill starts any Data Client task and points your agent at the skill for it. #4408, #4428, #4453

Debug the running app​

Ask your agent "why didn't this refetch?" or "what's in the cache right now?" and it can check the running app instead of guessing from code. In development, DevToolsManager puts the store on globalThis.__DC__: __DC__.controller is its Controller, and __DC__.actions its recent actions, recorded from the moment the app starts, with or without the Redux DevTools extension. Production builds don't include it.

Browser DevTools console
__DC__.controller.getState();
__DC__.actions.slice(-10);

The React and Vue skills read it from any browser tool that runs JavaScript in the page: Cursor's built-in browser, Chrome DevTools MCP, Playwright MCP or Claude in Chrome, and match dispatched actions to network requests; see debugging with agents. If your own scripts read __DC_CONTROLLERS__, switch them to __DC__. #4435, #4428, #4447

Vue skills​

Vue projects used to get the React hooks skill, so agents reached for React patterns. The data-client-vue skill writes DataClientPlugin and composable code and debugs a running app, and data-client-vue-testing writes renderDataCompose() and mountDataClient() tests that follow the Vue testing guides. #4126, #4181

llms.txt​

Agents without skills can read the docs as markdown, following the llms.txt convention:

Every page links its llms.txt in <head>, and every docs page links its markdown, so an agent given any page URL finds them on its own. The setup pages' markdown gives agents the setup steps directly. #4171, #4236, #4427

Skills match the docs​

Skill reference pages are generated per framework from these docs, so they carry shared snippets like install commands and give Vue projects only Vue code. A CI check fails when a skill drifts from the docs, so skills stay current with each release. #4170

Batch Controller.set()​

A Manager that receives a stream of rows (like price tickers over a websocket) can now write them all with one Controller.set() by passing an Array schema. Before v0.19, [Ticker] was a TypeScript error, so the usual workaround was a set() per row:

Before
ws.onmessage = event => {
const rows = JSON.parse(event.data);
for (const row of rows) {
ctrl.set(Ticker, { product_id: row.product_id }, row);
}
};
After
ws.onmessage = event => {
const rows = JSON.parse(event.data);
ctrl.set([Ticker], rows);
};

Each row merges with its stored entity, and entities not in the list are untouched. Array schemas take no args and no updater function. To batch mixed Entity types, deletes, or rows keyed by id, pass a Union, Invalidate, or Values schema; see Controller.set() for examples. #4103

Try both buttons below. This browser check starts from an empty store and times Promise.all of 500 set() calls against one batch set(). Both paths are one React commit, and each writes 500 new prices.

import React from 'react';
import { useController, useQuery } from '@data-client/react';
import { Ticker, newPrices } from './Ticker';

function PriceStream() {
  const ctrl = useController();
  const [timing, setTiming] = React.useState('');
  const first = useQuery(Ticker, { product_id: 'COIN-0' });

  const time = async (
    label: string,
    write: (rows: ReturnType<typeof newPrices>) => Promise<unknown>,
  ) => {
    const rows = newPrices();
    const start = performance.now();
    await write(rows);
    setTiming(`${label}: ${(performance.now() - start).toFixed(1)} ms`);
  };
  const perRow = () =>
    time('500 set() calls', rows =>
      Promise.all(
        rows.map(row => ctrl.set(Ticker, { product_id: row.product_id }, row)),
      ),
    );
  // highlight-next-line
  const batch = () => time('1 batch set()', rows => ctrl.set([Ticker], rows));

  return (
    <div>
      <button onClick={perRow}>set() per row</button>{' '}
      <button onClick={batch}>batch set()</button>
      <p>COIN-0: {first ? `$${first.price}` : 'no data yet'}</p>
      <p>{timing}</p>
    </div>
  );
}
render(<PriceStream />);
Pré-visualização

Performance​

Each set() is a separate store update, and every store update copies that entity type's table. Writing rows one at a time repeats that copy for every row, so the cost grows with both the batch size and the store size. A batch pays that copy once.

The chart is the node setMany benchmark in examples/benchmark/core.js: a store that already holds 500 entities, then a synchronous set() per row against one set([Ticker], rows). It measures the store update: 20x faster for 50 rows and 95x faster for 500 rows. #4103

Node setMany into a 500-entity store
50 rows20xOne set() per row 10.8 ms → set([Ticker], rows) 0.54 ms
500 rows95xOne set() per row 103 ms → set([Ticker], rows) 1.08 ms

Benchmarks over time | View setMany benchmark

In a Manager, buffer incoming messages and flush each batch with one set(), as described in Batching high-frequency updates. The coin app's StreamManager now flushes Coinbase ticker messages this way.

StreamManager.ts
handleMessage(msg: any) {
if (msg.type in this.entities) {
(this.buffer[msg.type] ??= {})[msg.product_id] = msg;
this.flushTimeout ??= setTimeout(this.flush, 50);
}
}

flush = () => {
const buffer = this.buffer;
this.buffer = {};
this.flushTimeout = undefined;
for (const type in buffer) {
this.controller.set([this.entities[type]], Object.values(buffer[type]));
}
};

Explore o exemplo coin-app

Mais demos

dica

If you filter DevToolsManager actions by schema, a batched write's action.schema is the Array schema, so match action.schema[0] for [Ticker] rather than the Entity itself.

Faster TypeScript​

TypeScript re-checks your code on every keystroke in the editor and on every CI build. In apps with many endpoints, heavy library types show up as laggy autocomplete, red squiggles that take seconds to appear, and slower builds. v0.19 makes RestEndpoint, resource() and .extend() much cheaper to check, with no code changes on your side. TypeScript still reports every error it reported before (#4173).

Results​

Our heaviest stress test, a file of 150 RestEndpoints with long paths, .extend() and .paginated(), now checks 2x faster on TypeScript 6 and 2.2x faster on TypeScript 7, using about 40% less memory. All numbers compare the published v0.18.1 packages with v0.19; check times are the best of 10 runs.

Checking 150 RestEndpoints with long paths
TypeScript 61.9xv0.18 3.83 s → v0.19 1.98 s
TypeScript 72.2xv0.18 1.5 s → v0.19 0.69 s
Memory for the same file
TypeScript 61.7xv0.18 365 MB → v0.19 211 MB
TypeScript 71.7xv0.18 220 MB → v0.19 128 MB

Endpoint-heavy code does much less type work. Type instantiations are TypeScript's unit of work: they're deterministic, so they compare cleanly across machines. Union does more work than in v0.18, because v0.19 now type-checks set() values (see below).

Type instantiations by stress test
Long paths2.4xv0.18 822 K → v0.19 341 K
Typical app1.5xv0.18 20.2 K → v0.19 13.4 K
React hooks1.2xv0.18 145 K → v0.19 122 K
Vue1.3xv0.18 62.6 K → v0.19 47.3 K
300 fields1.3xv0.18 17 K → v0.19 12.8 K
Schemas1.2xv0.18 118 K → v0.19 99.3 K
Union0.6xv0.18 9 K → v0.19 16.1 K

Hover or tap a cell for exact numbers.

TS 6 check timeTS 7 check timeTS 6 memory
Long paths
150 RestEndpoints with 6-param paths, .extend() and .paginated()
-48%
3.83s → 1.98s
-54%
1.5s → 0.69s
-42%
365MB → 211MB
Typical app
A few resources with .extend(), .paginated() and hooks
same
0.42s → 0.42s
-15%
0.062s → 0.053s
+3%
106MB → 109MB
React hooks
40 resources through every hook, plus ctrl.fetch() and ctrl.set()
-10%
1.24s → 1.12s
-7%
0.41s → 0.38s
-6%
167MB → 157MB
Vue
The same 40 resources through every composable
same
0.68s → 0.68s
-12%
0.17s → 0.15s
-4%
145MB → 139MB
300 fields
One Entity with 300 fields, read and updated 100 times
-14%
0.44s → 0.38s
-40%
0.086s → 0.052s
-9%
111MB → 101MB
Schemas
All, Query, Invalidate, Array, Object and Collection
-3%
1.1s → 1.07s
-14%
0.36s → 0.31s
-4%
156MB → 150MB
Union
A 30-member Union in a Collection and Values
-2%
0.42s → 0.41s
-4%
0.071s → 0.068s
-6%
107MB → 101MB
set() values
1000 ctrl.set() calls on a 30-member Union, a Collection of it and a 300-field Entity
-28%
1.49s → 1.07s
-54%
0.54s → 0.25s
-26%
176MB → 131MB
set() updaters
1000 ctrl.set(Union, args, prev => ...) updaters on a 30-member Union
-4%
3.58s → 3.43s
+11%
1.67s → 1.85s
+1%
381MB → 384MB

The set() rows measure typed set() values, which v0.18 didn't check at all. Plain values still check faster than before, and updater functions on large Unions check in about the same time even though TypeScript now checks each updater's return value (#4179).

Small files are dominated by TypeScript's fixed startup cost (loading lib.dom.d.ts alone takes about 100MB), so their time and memory barely move. The savings add up as a codebase grows.

What changed​

  • .extend() and .paginated() are shared across all endpoints instead of re-created for each endpoint type.
  • Endpoint options infer as plain object types, so TypeScript stops rebuilding them at every use.
  • Path parameters like /users/:id are read in a single pass.

Before shipping, we turned off every @ts-expect-error in the test suite on TypeScript 4.0 through 7 and confirmed every error still appears in the same place.

On TypeScript 5.x and earlier, a process(value, params) method passed to .extend() also no longer fails with "implicitly has an 'any' type" under strict.

Other improvements​

Typed set() values​

Controller.set() previously accepted any value for a schema, so a typo or a wrong field type only surfaced as bad data at runtime. Values are now typed by the schema: an Entity takes its fields, while a Collection, All or Array takes a list of rows. Every field is optional, since set() merges into what is already stored, and numbers and strings are interchangeable just like in API responses. #4133

Hover the red underlines to see each error.

import type { Controller } from '@data-client/react';
import { schema } from '@data-client/rest';
import { Todo, TodoResource } from './Todo';

export function updateTodos(ctrl: Controller) {
  // ✅ rows are partial Todos; ids may be strings or numbers
  ctrl.set(TodoResource.getList.schema, [{ id: '5', completed: true }]);
  ctrl.set(Todo, { id: 5 }, todo => ({ completed: !todo.completed }));
  ctrl.set([Todo], [{ id: 1, title: 'first' }, { id: 2 }]);

  // ❌ All takes a list of rows
  ctrl.set(new schema.All(Todo), 42);
  // ❌ completed is a boolean
  ctrl.set(Todo, { id: 5 }, { id: 5, completed: 'yes' });
  // ❌ Todo has no done field
  ctrl.set(TodoResource.getList.schema, [{ id: 5, done: true }]);
  // ❌ updaters must return Todo fields
  ctrl.set(Todo, { id: 5 }, todo => ({ title: todo.completed }));
}

A Query takes the input of the schema it wraps, since set() normalizes that schema rather than reversing process(). When each member declares its discriminator as a literal (like readonly type = 'post'), a Union row is checked against the member it selects, so it can't mix fields from different members; see Unions.

If code that previously compiled now fails here, it was writing data its schema doesn't describe. Fix the value, or widen the Entity's field types if the data really can take that shape.

Delete one entity with set()​

Passing an Invalidate schema to Controller.set() deletes the entity its row identifies, but the single form was a TypeScript error, so code wrapped it in a one-row batch. It now type-checks, with the row typed by the Entity's fields (#4230):

Before
ctrl.set([new Invalidate(Todo)], [{ id: '5' }]);
After
ctrl.set(new Invalidate(Todo), { id: '5' });

Like batch set(), it takes no args and no updater function.

Typed process() params​

A process() method passed to new RestEndpoint(), .extend() or resource().extend() got params typed as any, so reading a parameter the endpoint doesn't have compiled and returned undefined at runtime. params and body are now typed from the endpoint, including a path set in the same .extend() call (#4183, #4184):

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

const getUser = new RestEndpoint({ path: '/users/:id' });

export const getUserById = getUser.extend({
  path: '/users/by-id/:userId',
  process(value, params) {
    const userId: string | number = params.userId;
    // @ts-expect-error 'id' is not a param of '/users/by-id/:userId'
    params.id;
    return { ...value, userId };
  },
});

When every param is optional, params may be undefined, so read it with params?.page.

This can surface new TypeScript errors in existing process() methods. Each one marks a read that could be undefined or throw at runtime, so fix the param name or add the missing check.

Typed optimistic responses​

getOptimisticResponse() could return anything unless the endpoint had a typed process(), so a wrong field type only showed up as bad data in the UI until the server responded. The return value is now checked against the endpoint's schema, like a typed set() value: an Entity takes any of its fields, and a Collection takes a list of them (#4198):

import { Entity, RestEndpoint } from '@data-client/rest';

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

export const toggleTodo = new RestEndpoint({
  path: '/todos/:id',
  method: 'PATCH',
  schema: Todo,
  // @ts-expect-error completed is a boolean; 'false' is truthy, so the checkbox would stay checked
  getOptimisticResponse(snap, { id }) {
    const todo = snap.get(Todo, { id });
    if (!todo) throw snap.abort;
    return { id, completed: todo.completed ? 'false' : 'true' };
  },
});

This can surface new TypeScript errors in existing getOptimisticResponse() methods. Each one marks a value that would be stored with the wrong type until the server responds, so return the field's real type (here !todo.completed).

Typed push() on sorted Collections​

Wrapping a Collection in a Query to sort or filter it made push, unshift and remove reject every body except FormData, though they add to the list at runtime. The body is now typed from the Collection's Entity, so the cast can go and a wrong field is a TypeScript error. The same applies to a Collection wrapped in Lazy (#4226).

Before
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';

export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author } as any);
}
After
import { useController } from '@data-client/react';
import { getPosts } from './getPosts';

export function useAddPost() {
const ctrl = useController();
return (title: string, author: string) =>
ctrl.fetch(getPosts.push, { group: 'react' }, { title, author });
}

where getPosts sorts its Collection with a Query:

getPosts.ts
import { Collection, Entity, Query, RestEndpoint } from '@data-client/rest';

export class Post extends Entity {
id = '';
title = '';
group = '';
author = '';
}

export const getPosts = new RestEndpoint({
path: '/:group/posts',
searchParams: {} as { orderBy?: 'title' | 'author' },
schema: new Query(
new Collection([Post], { nonFilterArgumentKeys: /orderBy/ }),
(posts, { orderBy }: { orderBy?: 'title' | 'author' } = {}) =>
orderBy ?
[...posts].sort((a, b) => a[orderBy].localeCompare(b[orderBy]))
: posts,
),
});

FormData in resource().create​

resource()'s create sends a FormData body as-is, like update, partialUpdate and getList.push, but its type only allowed an object, so submitting a form needed a cast (#4235). Drop the cast:

Before
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget) as any);
}}
>
After
<form
onSubmit={e => {
e.preventDefault();
ctrl.fetch(PostResource.create, new FormData(e.currentTarget));
}}
>

create is deprecated; getList.push is its replacement and takes FormData too.

Vue getter arguments​

Vue composables like useSuspense() and useLive() were typed to accept a getter function as an argument, but they passed the function itself to the endpoint instead of calling it. Getters now work the same as refs and computed, and the composable refetches when anything the getter reads changes (#4115). Drop the computed() wrapper if you only used it to make arguments reactive:

Before
import { computed } from 'vue';

const props = defineProps<{ id: number }>();
const article = await useSuspense(
ArticleResource.get,
computed(() => ({ id: props.id })),
);
After
const props = defineProps<{ id: number }>();
const article = await useSuspense(ArticleResource.get, () => ({
id: props.id,
}));

This applies to every composable that takes endpoint arguments: useSuspense(), useLive(), useCache(), useDLE(), useFetch(), useQuery() and useSubscription().

Vue garbage collects by default​

Vue's DataClientPlugin only removed unused data if you passed a gcPolicy yourself. It now defaults to new GCPolicy(), the same as React's DataProvider, so you can drop the option unless you customize it (#4148).

Before
import { DataClientPlugin, GCPolicy } from '@data-client/vue';

app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
After
import { DataClientPlugin } from '@data-client/vue';

app.use(DataClientPlugin);

Data is removed only when no mounted component reads it and twice its freshness lifetime has passed since it was fetched (at least two minutes). A sweep checks every five minutes. Anything a component reads again before then is back in use.

A component that mounts after removal treats the data as never fetched: useSuspense() fetches it again, while useCache() returns undefined. To keep unused data longer or sweep less often, pass your own gcPolicy.

Vue stale-while-revalidate​

When a component mounts with data that is stale but still valid, Vue useSuspense() now renders it right away and refetches in the background, like React. Before, it showed the <Suspense> fallback until the refetch finished (#4169).

Entity classes typecheck as EntityInterface​

Helpers typed to accept any Entity with EntityInterface rejected Entity classes, because Entity.pk() typed its args as a mutable array. It is now readonly any[], so this typechecks (#4149):

import { Entity } from '@data-client/rest';
import type { EntityInterface } from '@data-client/react';

class User extends Entity {
  id = '';
  name = '';
}

function entityName(schema: EntityInterface) {
  return schema.key;
}

entityName(User);

If you override static pk() and type args as a mutable array, it still compiles, but a future breaking release will require readonly; update it now.

Deleted entities read as undefined​

When an entity was deleted and its refetch failed, useCache() and useDLE() (React and Vue) returned an internal Symbol as data. A Symbol is truthy, so the usual "not loaded yet" guard let it through and the component rendered as if it had an entity (#4150):

function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
// a deleted todo used to get past this guard, so todo.title.trim() threw
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}

Now data is undefined, matching its type and Controller.get(). The same applies to Controller.getResponse() and Controller.fetchIfStale(), for example in custom Managers.

If you worked around this, the workaround can go. A plain truthiness check is enough:

Before
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo || typeof todo === 'symbol') return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}
After
function TodoDetail({ id }: { id: number }) {
const todo = useCache(TodoResource.get, { id });
if (!todo) return <TodoPlaceholder />;
return <h3>{todo.title.trim()}</h3>;
}

To show something specific when the refetch failed (for example a 404 after deletion), read error from useDLE() rather than inspecting data.

Faster Redux DevTools​

With the Redux DevTools extension open, every store update in development serializes the whole store for the extension, and each timestamp in it was formatted the slow way. Large stores or frequent updates, like polling, live data or many controller.set() calls, made the page stutter. Each update now serializes 40-60x faster, and timestamps still read like 10:42:07.123 AM (#4163). Production builds don't include DevTools, so they are unaffected.

DevTools serialization per store update
50 entities60xv0.18 18.6 ms → v0.19 0.31 ms
500 entities40xv0.18 201 ms → v0.19 5 ms

Migration guide​

This upgrade requires updating all package versions simultaneously.

npm install --save @data-client/react@^0.19.0 @data-client/rest@^0.19.0 @data-client/endpoint@^0.19.0 @data-client/core@^0.19.0 @data-client/vue@^0.19.0 @data-client/test@^0.19.0 @data-client/img@^0.19.0
Execute no seu terminal

TypeScript 4.0 or later​

Skip this section if you already use TypeScript 4.0 or later.

The TypeScript 3.x declarations are removed, since they no longer typechecked on any TypeScript 3.x version. Bump typescript to ^4.0.0 or later in your devDependencies. #4151

Manager.middleware is required​

Skip this section if your Managers already define middleware.

getMiddleware() was replaced by the middleware property in v0.14; it's now removed. A Manager that still defines it, or has no middleware at all, is a TypeScript error, and throws when DataProvider or DataClientPlugin mounts in development. Rename getMiddleware() to a middleware property:

Before
class LoggingManager implements Manager {
getMiddleware = (): Middleware => controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
};

cleanup() {}
}
After
class LoggingManager implements Manager {
middleware: Middleware = controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
};

cleanup() {}
}

#4382

@data-client/test requires @data-client/react 0.15.3​

Skip this section if you don't use @data-client/test.

@data-client/test now builds on @data-client/react/mock, which first loads in Jest and other CommonJS tests in @data-client/react 0.15.3, so support for 0.12 through 0.15.2 is dropped. Upgrade @data-client/react to the same version as @data-client/test, as the install command above does. Fixture types imported from @data-client/test and from @data-client/react/mock are now the same types. #4414

__DC_CONTROLLERS__ is now __DC__​

Skip this section if no debugging script or tool of yours reads globalThis.__DC_CONTROLLERS__.

In development, DevToolsManager now puts the store on globalThis.__DC__, whose controller and actions read the page's store, instead of the __DC_CONTROLLERS__ Map. With several stores, __DC__.stores lists their keys and setting __DC__.store picks one.

Before
__DC_CONTROLLERS__.get('Data Client: My App').getState();
After
__DC__.store = 'Data Client: My App'; // only needed with several stores
__DC__.controller.getState();

#4435

Get ready for future releases​

Nothing here is needed to upgrade to 0.19; your code keeps working as is. Each item will break in a future release, so updating now makes that upgrade a no-op.

Entity.pk() args are readonly​

Skip this if you don't override static pk() with a typed args parameter.

Entity.pk() now types args as readonly any[]. Overrides that annotate it as a mutable array still compile for now, but a future release will require readonly.

Before
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}
After
class User extends Entity {
static pk(value: any, parent?: any, key?: string, args?: readonly any[]) {
return `${value.id}-${args?.[0]?.org}`;
}
}

Vue createDataClient()​

Skip this if you install the store with app.use(DataClientPlugin).

createDataClient() and ProvidedDataClient are internal to DataClientPlugin and will stop being exported. Install the plugin instead, and read the controller with useController(). #4193

Before
import { createDataClient } from '@data-client/vue';

const { controller } = createDataClient({ managers, initialState });
After
import { DataClientPlugin, useController } from '@data-client/vue';

app.use(DataClientPlugin, { managers, initialState });
// in a component's setup()
const controller = useController();

Vue renderDataCompose() waitForNextUpdate()​

Skip this if your Vue tests don't call waitForNextUpdate().

waitForNextUpdate() from renderDataCompose() gives up silently after 1 second, so a test can pass while the composable is still suspended. It will be removed. await the result instead, and call allSettled() after changing props or calling the controller; it now also waits for the fetch a prop change starts. #4221

Before
const { result, waitForNextUpdate } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
await waitForNextUpdate();
const article = await result;

props.id = 6;
await waitForNextUpdate();
After
const { result, allSettled } = await renderDataCompose(
(props: { id: number }) =>
useSuspense(ArticleResource.get, () => ({ id: props.id })),
{ props, resolverFixtures },
);
const article = await result;

props.id = 6;
await allSettled();

Upgrade support​

As usual, if you have any troubles or questions, feel free to join our Chat or file a bug