Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ ctrl.doSomething();

### gcPolicy?: GCInterface {#gcPolicy}

Removes data from the store once no component uses it and it has gone stale. By default, nothing
is ever removed.
Removes data from the store once no component uses it and it has gone stale. Defaults to
`new GCPolicy()`; pass one to change how often it sweeps or how long unused data is kept.

```ts title="main.ts"
import { DataClientPlugin, GCPolicy } from '@data-client/vue';
Expand Down
4 changes: 2 additions & 2 deletions .agents/skills/data-client-vue/references/DataClientPlugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,8 @@ ctrl.doSomething();

### gcPolicy?: GCInterface {#gcPolicy}

Removes data from the store once no component uses it and it has gone stale. By default, nothing
is ever removed.
Removes data from the store once no component uses it and it has gone stale. Defaults to
`new GCPolicy()`; pass one to change how often it sweeps or how long unused data is kept.

```ts title="main.ts"
import { DataClientPlugin, GCPolicy } from '@data-client/vue';
Expand Down
25 changes: 25 additions & 0 deletions .changeset/vue-default-gcpolicy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@data-client/vue': patch
---

`DataClientPlugin` garbage collects by default

Previously, unused data was only removed from the store if you passed a `gcPolicy` yourself. It now defaults
to `new GCPolicy()`, matching `DataProvider` in `@data-client/react`, so you can drop the option unless you
customize it.

Before

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

app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
```

After

```ts
import { DataClientPlugin } from '@data-client/vue';

app.use(DataClientPlugin);
```
4 changes: 2 additions & 2 deletions docs/core/api/DataClientPlugin.vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ ctrl.doSomething();

### gcPolicy?: GCInterface {#gcPolicy}

Removes data from the store once no component uses it and it has gone stale. By default, nothing
is ever removed.
Removes data from the store once no component uses it and it has gone stale. Defaults to
`new GCPolicy()`; pass one to change how often it sweeps or how long unused data is kept.

```ts title="main.ts"
import { DataClientPlugin, GCPolicy } from '@data-client/vue';
Expand Down
46 changes: 45 additions & 1 deletion packages/vue/src/__tests__/integration-garbage-collection.web.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { GCPolicy } from '@data-client/core';
import { defineComponent, h } from 'vue';
import { defineComponent, h, reactive } from 'vue';

import { Article, ArticleResource } from '../../../../__tests__/new';
import useFetch from '../consumers/useFetch';
Expand All @@ -16,6 +16,7 @@ describe('Integration Garbage Collection Web (Vue)', () => {
});
afterEach(() => {
infoSpy.mockRestore();
jest.useRealTimers();
});

it('should initialize with GCPolicy', () => {
Expand Down Expand Up @@ -191,4 +192,47 @@ describe('Integration Garbage Collection Web (Vue)', () => {

cleanup2();
});

it('removes unused stale data without passing a gcPolicy', async () => {
jest.useFakeTimers();
const ArticleDetail = defineComponent({
name: 'ArticleDetail',
setup() {
const article = useQuery(Article, { id: 1 });
return () => h('div', article.value?.title ?? 'missing');
},
});
const props = reactive({ show: true });
const TestComp = defineComponent({
name: 'TestComp',
props: ['show'],
setup(props) {
return () => (props.show ? h(ArticleDetail) : h('div', 'blank'));
},
});

const { wrapper, controller, cleanup } = mountDataClient(TestComp, {
props,
initialFixtures: [
{
endpoint: ArticleResource.get,
args: [{ id: 1 }],
response: { id: 1, title: 'Test Article', content: 'Content' },
},
],
});
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('Test Article');

props.show = false;
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('blank');

// default GCPolicy sweeps every 5 minutes; data must also be stale
jest.advanceTimersByTime(60 * 1000 * 5);
await Promise.resolve();
expect(controller.getState().entities.Article?.['1']).toBeUndefined();

cleanup();
});
});
7 changes: 6 additions & 1 deletion packages/vue/src/providers/createDataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
applyManager,
initManager,
createReducer,
GCPolicy,
} from '@data-client/core';
import type { State, Manager, GCInterface } from '@data-client/core';
import { provide, shallowRef, type ShallowRef, type App } from 'vue';
Expand Down Expand Up @@ -40,7 +41,11 @@ export interface ProvidedDataClient {
export function createDataClient(
options: ProvideOptions = {},
): ProvidedDataClient {
const { Controller = DataController, gcPolicy, app } = options;
const {
Controller = DataController,
gcPolicy = new GCPolicy(),
app,
} = options;

// stable singletons for this provider scope
const controller = new Controller({ gcPolicy });
Expand Down
46 changes: 46 additions & 0 deletions website/blog/2026-10-03-v0.19-batch-set.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ endpoint code about 2x faster, and fixes a round of TypeScript and Vue issues.
- Vue [useSuspense()](/vue/api/useSuspense) [shows stale data on mount](/blog/2026/10/03/v0.19-batch-set#vue-stale-while-revalidate) and refetches in the background, like React, instead of showing the `<Suspense>` fallback until the refetch finishes ([#4169](https://github.com/reactive/data-client/pull/4169))
- Vue [useDLE()](/vue/api/useDLE) and [useCache()](/vue/api/useCache) keep expired `invalidIfStale` data through unrelated store updates instead of getting stuck loading ([#4142](https://github.com/reactive/data-client/pull/4142))
- Vue `DataClientPlugin` installs on Vue versions before 3.5 instead of throwing `app.onUnmount is not a function` ([#4146](https://github.com/reactive/data-client/pull/4146))
- Vue [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) garbage collects by default, matching React's [DataProvider](/docs/api/DataProvider), so you no longer need to pass `gcPolicy`; [how it decides what to remove](/blog/2026/10/03/v0.19-batch-set#vue-gc) ([#4148](https://github.com/reactive/data-client/pull/4148))
- Vue composables accept [getter arguments](/blog/2026/10/03/v0.19-batch-set#vue-getter-args) like `() => ({ id: props.id })`; they were typed to allow them but passed the function itself to the endpoint ([#4115](https://github.com/reactive/data-client/pull/4115))
- Fix `Cannot find name 'NoInfer'` and `export type` errors on TypeScript 4.x with `skipLibCheck` off ([#4138](https://github.com/reactive/data-client/pull/4138))
- Fix `Entity`, `Endpoint`, `Union` and `RestEndpoint` type errors on TypeScript 4.0–4.5 with `skipLibCheck` off ([#4140](https://github.com/reactive/data-client/pull/4140))
Expand Down Expand Up @@ -420,6 +421,51 @@ This applies to every composable that takes endpoint arguments: [useSuspense()](
[useLive()](/vue/api/useLive), [useCache()](/vue/api/useCache), [useDLE()](/vue/api/useDLE),
[useFetch()](/vue/api/useFetch), [useQuery()](/vue/api/useQuery) and [useSubscription()](/vue/api/useSubscription).

### Vue garbage collects by default {#vue-gc}

Vue's [DataClientPlugin](/vue/api/DataClientPlugin#gcPolicy) only removed unused data if you passed a `gcPolicy`
yourself. It now defaults to `new GCPolicy()`, the same as React's [DataProvider](/docs/api/DataProvider), so you can
drop the option unless you customize it ([#4148](https://github.com/reactive/data-client/pull/4148)).

<DiffEditor>

```ts title="Before"
import { DataClientPlugin, GCPolicy } from '@data-client/vue';

app.use(DataClientPlugin, { gcPolicy: new GCPolicy() });
```

```ts title="After"
import { DataClientPlugin } from '@data-client/vue';

app.use(DataClientPlugin);
```

</DiffEditor>

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.

<center>

<div style={{maxWidth:'640px'}}>

```mermaid
flowchart LR
inuse("In use") -- "last reader unmounts" --> unused("Unused")
unused -- "2x lifetime passes" --> old("Unused and old")
old -- "next sweep" --> removed("Removed")
```

</div>

</center>

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

### Vue stale-while-revalidate {#vue-stale-while-revalidate}

When a component mounts with data that is [stale](/docs/concepts/expiry-policy#stale) but still valid, Vue
Expand Down
Loading