diff --git a/.agents/skills/data-client-setup/references/DataClientPlugin.md b/.agents/skills/data-client-setup/references/DataClientPlugin.md index 740f3ec855f1..dc56592955e0 100644 --- a/.agents/skills/data-client-setup/references/DataClientPlugin.md +++ b/.agents/skills/data-client-setup/references/DataClientPlugin.md @@ -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'; diff --git a/.agents/skills/data-client-vue/references/DataClientPlugin.md b/.agents/skills/data-client-vue/references/DataClientPlugin.md index 2fe9c1909427..fd97f0763115 100644 --- a/.agents/skills/data-client-vue/references/DataClientPlugin.md +++ b/.agents/skills/data-client-vue/references/DataClientPlugin.md @@ -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'; diff --git a/.changeset/vue-default-gcpolicy.md b/.changeset/vue-default-gcpolicy.md new file mode 100644 index 000000000000..0bf139ffba2a --- /dev/null +++ b/.changeset/vue-default-gcpolicy.md @@ -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); +``` diff --git a/docs/core/api/DataClientPlugin.vue.md b/docs/core/api/DataClientPlugin.vue.md index bde3e4a57415..a9d8a98ac951 100644 --- a/docs/core/api/DataClientPlugin.vue.md +++ b/docs/core/api/DataClientPlugin.vue.md @@ -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'; diff --git a/packages/vue/src/__tests__/integration-garbage-collection.web.ts b/packages/vue/src/__tests__/integration-garbage-collection.web.ts index 64f76564566d..f5878b989d91 100644 --- a/packages/vue/src/__tests__/integration-garbage-collection.web.ts +++ b/packages/vue/src/__tests__/integration-garbage-collection.web.ts @@ -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'; @@ -16,6 +16,7 @@ describe('Integration Garbage Collection Web (Vue)', () => { }); afterEach(() => { infoSpy.mockRestore(); + jest.useRealTimers(); }); it('should initialize with GCPolicy', () => { @@ -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(); + }); }); diff --git a/packages/vue/src/providers/createDataClient.ts b/packages/vue/src/providers/createDataClient.ts index 0c67683b4ecb..988b36b7c254 100644 --- a/packages/vue/src/providers/createDataClient.ts +++ b/packages/vue/src/providers/createDataClient.ts @@ -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'; @@ -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 }); diff --git a/website/blog/2026-10-03-v0.19-batch-set.md b/website/blog/2026-10-03-v0.19-batch-set.md index e8a1764d5873..46ecdb184028 100644 --- a/website/blog/2026-10-03-v0.19-batch-set.md +++ b/website/blog/2026-10-03-v0.19-batch-set.md @@ -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 `` 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)) @@ -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)). + + + +```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); +``` + + + +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. + +
+ +
+ +```mermaid +flowchart LR + inuse("In use") -- "last reader unmounts" --> unused("Unused") + unused -- "2x lifetime passes" --> old("Unused and old") + old -- "next sweep" --> removed("Removed") +``` + +
+ +
+ +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