From cdfa74fe9155618e2d2d3129c7ff8e7899b06644 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 3 Oct 2026 23:43:33 +0000 Subject: [PATCH 01/15] docs(skills): Add data-client-vue agent skill Vue readers were pointed at the React hooks skill. Adds a Vue skill covering DataClientPlugin, composables, reactive arguments, Suspense and onErrorCaptured boundaries, and lists it in the agent skills and debugging docs. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WEpX2AVQj3Xmn2Q51om3J4 --- .../references/devtools-debugging.md | 8 +- .agents/skills/data-client-vue/SKILL.md | 242 ++++++++++++++++++ .../skills/data-client-vue/evals/evals.json | 33 +++ .../data-client-vue/references/Actions.md | 1 + .../data-client-vue/references/Controller.md | 1 + .../references/_AsyncBoundary.md | 1 + .../data-client-vue/references/_VoteDemo.md | 1 + .../data-client-vue/references/_pagination.md | 1 + .../data-client-vue/references/_useLive.md | 1 + .../data-client-vue/references/_useLoading.md | 1 + .../references/data-dependency.md | 1 + .../references/devtools-debugging.md | 1 + .../references/getDefaultManagers.md | 1 + .../data-client-vue/references/mutations.md | 1 + .../data-client-vue/references/useCache.md | 1 + .../references/useController.md | 1 + .../data-client-vue/references/useDLE.md | 1 + .../data-client-vue/references/useDebounce.md | 1 + .../data-client-vue/references/useFetch.md | 1 + .../data-client-vue/references/useLive.md | 1 + .../data-client-vue/references/useLoading.md | 1 + .../data-client-vue/references/useQuery.md | 1 + .../references/useSubscription.md | 1 + .../data-client-vue/references/useSuspense.md | 1 + docs/core/getting-started/agent-skills.md | 2 + docs/core/getting-started/debugging.md | 4 +- 26 files changed, 304 insertions(+), 6 deletions(-) create mode 100644 .agents/skills/data-client-vue/SKILL.md create mode 100644 .agents/skills/data-client-vue/evals/evals.json create mode 120000 .agents/skills/data-client-vue/references/Actions.md create mode 120000 .agents/skills/data-client-vue/references/Controller.md create mode 120000 .agents/skills/data-client-vue/references/_AsyncBoundary.md create mode 120000 .agents/skills/data-client-vue/references/_VoteDemo.md create mode 120000 .agents/skills/data-client-vue/references/_pagination.md create mode 120000 .agents/skills/data-client-vue/references/_useLive.md create mode 120000 .agents/skills/data-client-vue/references/_useLoading.md create mode 120000 .agents/skills/data-client-vue/references/data-dependency.md create mode 120000 .agents/skills/data-client-vue/references/devtools-debugging.md create mode 120000 .agents/skills/data-client-vue/references/getDefaultManagers.md create mode 120000 .agents/skills/data-client-vue/references/mutations.md create mode 120000 .agents/skills/data-client-vue/references/useCache.md create mode 120000 .agents/skills/data-client-vue/references/useController.md create mode 120000 .agents/skills/data-client-vue/references/useDLE.md create mode 120000 .agents/skills/data-client-vue/references/useDebounce.md create mode 120000 .agents/skills/data-client-vue/references/useFetch.md create mode 120000 .agents/skills/data-client-vue/references/useLive.md create mode 120000 .agents/skills/data-client-vue/references/useLoading.md create mode 120000 .agents/skills/data-client-vue/references/useQuery.md create mode 120000 .agents/skills/data-client-vue/references/useSubscription.md create mode 120000 .agents/skills/data-client-vue/references/useSuspense.md diff --git a/.agents/skills/data-client-react/references/devtools-debugging.md b/.agents/skills/data-client-react/references/devtools-debugging.md index 616c48c87fba..a6551fccbb53 100644 --- a/.agents/skills/data-client-react/references/devtools-debugging.md +++ b/.agents/skills/data-client-react/references/devtools-debugging.md @@ -1,12 +1,12 @@ -# Debugging @data-client/react with Chrome DevTools MCP +# Debugging Data Client with Chrome DevTools MCP -Debug `@data-client/react` state and actions programmatically via Chrome DevTools MCP `evaluate_script`. The app's `DevToolsManager` exposes the Controller on `globalThis.__DC_CONTROLLERS__` (a `Map` keyed by `devtoolsName`) in dev mode. +Debug `@data-client/react` or `@data-client/vue` state and actions programmatically via Chrome DevTools MCP `evaluate_script`. The app's `DevToolsManager` exposes the Controller on `globalThis.__DC_CONTROLLERS__` (a `Map` keyed by `devtoolsName`) in dev mode. ## Prerequisites 1. Dev server running with `NODE_ENV !== 'production'` 2. Chrome DevTools MCP connected and page loaded -3. `DevToolsManager` included in `DataProvider` managers (default in dev mode) +3. `DevToolsManager` included in `DataProvider` (React) or `DataClientPlugin` (Vue) managers (default in dev mode) ## Step 1: Access the Controller @@ -26,7 +26,7 @@ Debug `@data-client/react` state and actions programmatically via Chrome DevTool ### Get a controller by key -Use the key from discovery. Always use `.get(devtoolsName)` with the actual key — not `.values().next().value` — so you target the correct store when multiple `DataProvider`s exist. +Use the key from discovery. Always use `.get(devtoolsName)` with the actual key — not `.values().next().value` — so you target the correct store when multiple `DataProvider`s or Vue apps exist. ```js // evaluate_script diff --git a/.agents/skills/data-client-vue/SKILL.md b/.agents/skills/data-client-vue/SKILL.md new file mode 100644 index 000000000000..f9582ac9a442 --- /dev/null +++ b/.agents/skills/data-client-vue/SKILL.md @@ -0,0 +1,242 @@ +--- +name: data-client-vue +description: Use @data-client/vue composables for data fetching, mutations, and rendering - useSuspense, useFetch, useQuery, useCache, useLive, useDLE, useSubscription, useController, useLoading, useDebounce, DataClientPlugin, Suspense, onErrorCaptured. Use when reading/rendering remote data, triggering mutations, doing optimistic updates, real-time subscriptions, or wiring Suspense and error handling in Vue 3. +license: Apache 2.0 +--- +## Setup + +Install `DataClientPlugin` once on the app. Composables only work inside ` + + +``` + +Use `computed()` or `ref()` rather than a bare getter function (`() => ({ id })`). + +## Mutations + +```ts +const ctrl = useController(); +// PUT https://jsonplaceholder.typicode.com/todos/5 +const updateTodo = todo => ctrl.fetch(TodoResource.update, { id }, todo); +// PATCH https://jsonplaceholder.typicode.com/todos/5 +const partialUpdateTodo = todo => + ctrl.fetch(TodoResource.partialUpdate, { id }, todo); +// POST https://jsonplaceholder.typicode.com/todos +const addTodoToBeginning = todo => + ctrl.fetch(TodoResource.getList.unshift, todo); +// POST https://jsonplaceholder.typicode.com/todos?userId=1 +const addTodoToEnd = todo => ctrl.fetch(TodoResource.getList.push, { userId: 1 }, todo); +// DELETE https://jsonplaceholder.typicode.com/todos/5 +const deleteTodo = id => ctrl.fetch(TodoResource.delete, { id }); +// GET https://jsonplaceholder.typicode.com/todos?userId=1&page=2 +const getNextPage = (page) => ctrl.fetch(TodoResource.getList.getPage, { userId: 1, page }) +``` + +Read reactive values (`props.todo.id`, `title.value`) inside the handler so each call uses current state. +In Options API templates the Controller is also available as `$dataClient`. + +## Helpful composables + +```ts +const ctrl = useController(); +const [handleSubmit, loading, error] = useLoading(async (data: FormData) => { + const post = await ctrl.fetch(PostResource.getList.push, data); + router.push(`/posts/${post.id}`); +}); +// loading and error are refs +``` + +```vue + + + +``` + +## Loading and error boundaries + +There is no `AsyncBoundary` component in Vue. A component that `await`s `useSuspense()` or `useLive()` +must render inside Vue's [``](https://vuejs.org/guide/built-ins/suspense.html); its +`#fallback` slot is the loading state. Catch fetch errors in an ancestor with +[`onErrorCaptured()`](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured). +Reuse the codebase's existing boundary component if it has one. + +```vue title="AsyncBoundary.vue" + + + +``` + +Place boundaries around route views or sections, not around each data-bound component. + +## Type-safe imperative actions + +[Controller](references/Controller.md) is returned from `useController()`. It has: +ctrl.fetch(), ctrl.fetchIfStale(), ctrl.expireAll(), ctrl.invalidate(), ctrl.invalidateAll(), ctrl.setResponse(), ctrl.set(), +ctrl.setError(), ctrl.resetEntireStore(), ctrl.subscribe(), ctrl.unsubscribe(). + +Write many entities without a fetch with one `ctrl.set([Entity], rows)`. Never loop `ctrl.set(Entity, args, row)` per row, and never add an endpoint or `setResponse()` just to batch. + +## Programmatic queries + +```ts +const queryRemainingTodos = new Query( + TodoResource.getList.schema, + entries => entries.filter(todo => !todo.completed).length, +); + +const allRemainingTodos = useQuery(queryRemainingTodos); +const firstUserRemainingTodos = useQuery(queryRemainingTodos, { userId: 1 }); +``` + +```ts +const groupTodoByUser = new Query( + TodoResource.getList.schema, + todos => Object.groupBy(todos, todo => todo.userId), +); +const todosByUser = useQuery(groupTodoByUser); +``` + +--- + +## Browser Debugging (Chrome DevTools MCP) + +To inspect store state, track dispatched [actions](references/Actions.md), or invoke +[Controller](references/Controller.md) methods from a browser MCP (`user-chrome-devtools`), +see [devtools-debugging](references/devtools-debugging.md). Uses `globalThis.__DC_CONTROLLERS__` +available in dev mode. + +## Managers + +Custom [Managers](https://dataclient.io/docs/api/Manager) allow for global side effect handling. +This is useful for websockets, SSE, logging, etc. Pass them to `DataClientPlugin` with +[getDefaultManagers](references/getDefaultManagers.md). Always use the skill "data-client-manager" when writing managers. + +## Best Practices & Notes + +- [useDebounce(query, timeout)](references/useDebounce.md) when rendering async data based on user field inputs +- [[handleSubmit, loading, error] = useLoading()](references/useLoading.md) when tracking async mutations +- Prefer smaller Vue components that do one thing +- **Co-locate data bindings**: call useSuspense/useDLE/useCache/useQuery in the component that renders the data — don't prop drill +- **Don't hide data bindings inside custom composables**: wrapping them obfuscates a component's data dependencies and couples data logic to view code, causing drift. Put tightly coupled data transformations in a `Query` schema (with the data model, e.g. `src/resources/`) so they stay reusable and evolve independently of views +- Don't copy results into `ref()`/`reactive()` state; render the returned refs directly so updates flow through +- For tests, apply the skill "data-client-vue-testing" + +# References + +For detailed API documentation, see the [references](references/) directory. They cover React and Vue; +read the `:::vue` sections. + +- [useSuspense](references/useSuspense.md);[_pagination.md](references/_pagination.md) - Fetch with Suspense +- [useFetch](references/useFetch.md) - Start fetches early for parallel loading +- [useQuery](references/useQuery.md) - Read from cache without fetch +- [useCache](references/useCache.md) - Read from cache (nullable) +- [useLive](references/useLive.md);[_useLive.md](references/_useLive.md) - Fetch + subscribe to updates +- [useDLE](references/useDLE.md) - Fetch without Suspense (returns data/loading/error) +- [useSubscription](references/useSubscription.md) - Subscribe to updates (polling/websocket/SSE) +- [useController](references/useController.md) - Access Controller +- [Controller](references/Controller.md) - Imperative actions +- [_AsyncBoundary.md](references/_AsyncBoundary.md) - Suspense and onErrorCaptured boundaries +- [useLoading](references/useLoading.md);[_useLoading.md](references/_useLoading.md) - Track async mutation state +- [useDebounce](references/useDebounce.md) - Debounce values +- [getDefaultManagers](references/getDefaultManagers.md) - Configure `DataClientPlugin` managers +- [data-dependency](references/data-dependency.md) - Rendering guide +- [mutations](references/mutations.md);[_VoteDemo.md](references/_VoteDemo.md) - Mutations guide +- [Actions](references/Actions.md) - Store action types (FETCH, SET, etc.) +- [devtools-debugging](references/devtools-debugging.md) - Debug with Chrome DevTools MCP + +**ALWAYS follow these patterns and refer to the official docs for edge cases. Prioritize code generation that is idiomatic, type-safe, and leverages automatic normalization/caching via skill "data-client-schema" definitions.** diff --git a/.agents/skills/data-client-vue/evals/evals.json b/.agents/skills/data-client-vue/evals/evals.json new file mode 100644 index 000000000000..9c3f60603c15 --- /dev/null +++ b/.agents/skills/data-client-vue/evals/evals.json @@ -0,0 +1,33 @@ +{ + "skill_name": "data-client-vue", + "evals": [ + { + "id": 1, + "name": "csv-import", + "prompt": "Add an `ImportProducts.vue` component (src/components/ImportProducts.vue) with a file input. When the user picks a CSV, parse it with `parseProductsCsv(file): Promise<{ id: string; name: string; price: number }[]>` from src/lib/csv.ts and put every row into the Data Client store, so `ProductRow.vue` (which calls `useQuery(Product, { id })`) shows them. There is no server call for this. Product is in src/resources/Product.ts and extends Entity with `id`, `name`, `price`.", + "expected_output": "A - - ``` -Use `computed()` or `ref()` rather than a bare getter function (`() => ({ id })`). - When arguments change, `useSuspense()` does not suspend again: the result is `undefined` until the new fetch resolves (unless that data is already cached). Guard the template (`v-if="todo"`), or have the parent remount the component with `:key="id"` so it suspends inside `` again. @@ -120,22 +96,10 @@ const [handleSubmit, loading, error] = useLoading(async (data: FormData) => { // loading and error are refs ``` -```vue - - - +// pass debouncedQuery to the component that fetches, inside ``` ## Loading and error boundaries @@ -144,29 +108,10 @@ There is no `AsyncBoundary` component in Vue. A component that `await`s `useSusp must render inside Vue's [``](https://vuejs.org/guide/built-ins/suspense.html); its `#fallback` slot is the loading state. Catch fetch errors in an ancestor with [`onErrorCaptured()`](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured). -Reuse the codebase's existing boundary component if it has one. - -```vue title="AsyncBoundary.vue" - - - -``` - -Place boundaries around route views or sections, not around each data-bound component. +Reuse the codebase's existing boundary component if it has one; otherwise follow the +[boundary example](references/_AsyncBoundary.md) (`onErrorCaptured` stores the error and returns `false`; +the template shows it or renders ``). Place boundaries around route views or sections, not around +each data-bound component ([boundaries](references/data-dependency.md#boundaries)). ## Type-safe imperative actions @@ -238,6 +183,7 @@ read the `:::vue` sections. - [_AsyncBoundary.md](references/_AsyncBoundary.md) - Suspense and onErrorCaptured boundaries - [useLoading](references/useLoading.md);[_useLoading.md](references/_useLoading.md) - Track async mutation state - [useDebounce](references/useDebounce.md) - Debounce values +- [installation](references/installation.md) - Install `DataClientPlugin` - [getDefaultManagers](references/getDefaultManagers.md) - Configure `DataClientPlugin` managers - [data-dependency](references/data-dependency.md) - Rendering guide - [mutations](references/mutations.md);[_VoteDemo.md](references/_VoteDemo.md) - Mutations guide diff --git a/.agents/skills/data-client-vue/evals/evals.json b/.agents/skills/data-client-vue/evals/evals.json index 257d8b10fa09..55d5034a2369 100644 --- a/.agents/skills/data-client-vue/evals/evals.json +++ b/.agents/skills/data-client-vue/evals/evals.json @@ -23,10 +23,9 @@ "files": [], "assertions": [ "Both `useSuspense` calls are awaited at the top level of ` + + +``` diff --git a/.agents/skills/data-client-vue/references/_VoteDemo.md b/.agents/skills/data-client-vue/references/_VoteDemo.md deleted file mode 120000 index f3bd45e85171..000000000000 --- a/.agents/skills/data-client-vue/references/_VoteDemo.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/shared/_VoteDemo.mdx \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/_VoteDemo.md b/.agents/skills/data-client-vue/references/_VoteDemo.md new file mode 100644 index 000000000000..cf888d755a76 --- /dev/null +++ b/.agents/skills/data-client-vue/references/_VoteDemo.md @@ -0,0 +1,124 @@ + + +```ts title="Post" +import { Entity, schema } from '@data-client/rest'; + +export class Post extends Entity { + id = 0; + author = { id: 0 }; + title = ''; + body = ''; + votes = 0; + + static key = 'Post'; + + static schema = { + author: EntityMixin( + class User { + id = 0; + }, + ), + }; + + get img() { + return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`; + } +} +``` + +```ts title="PostResource" {15-22} +import { resource } from '@data-client/rest'; +import { Post } from './Post'; + +export { Post }; + +export const PostResource = resource({ + path: '/posts/:id', + searchParams: {} as { userId?: string | number } | undefined, + schema: Post, +}).extend('vote', { + path: '/posts/:id/vote', + method: 'POST', + body: undefined, + schema: Post, + getOptimisticResponse(snapshot, { id }) { + const post = snapshot.get(Post, { id }); + if (!post) throw snapshot.abort; + return { + id, + votes: post.votes + 1, + }; + }, +}); +``` + +```html title="PostItem.vue" {9} + + + +``` + +```html title="TotalVotes.vue" {13} + + + +``` + +```html title="PostList.vue" + + + +``` diff --git a/.agents/skills/data-client-vue/references/_pagination.md b/.agents/skills/data-client-vue/references/_pagination.md deleted file mode 120000 index 44c3e22eb030..000000000000 --- a/.agents/skills/data-client-vue/references/_pagination.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/shared/_pagination.mdx \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/_pagination.md b/.agents/skills/data-client-vue/references/_pagination.md new file mode 100644 index 000000000000..f258e6b2e6bc --- /dev/null +++ b/.agents/skills/data-client-vue/references/_pagination.md @@ -0,0 +1,108 @@ + + +```ts title="User" +import { Entity } from '@data-client/rest'; + +export class User extends Entity { + id = 0; + name = ''; + username = ''; + email = ''; + phone = ''; + website = ''; + + get profileImage() { + return `https://i.pravatar.cc/64?img=${this.id + 4}`; + } + + pk() { + return this.id; + } + static key = 'User'; +} +``` + +```ts title="Post" {22,24} +import { Entity, resource, Collection } from '@data-client/rest'; +import { User } from './User'; + +export class Post extends Entity { + id = 0; + author = User.fromJS(); + title = ''; + body = ''; + + pk() { + return this.id; + } + static key = 'Post'; + + static schema = { + author: User, + }; +} +export const PostResource = resource({ + path: '/posts/:id', + schema: Post, + paginationField: 'cursor', +}).extend('getList', { + schema: { posts: new Collection([Post]), cursor: '' }, +}); +``` + +```html title="PostItem.vue" + + + +``` + +```html title="LoadMore.vue" + + + +``` + +```html title="PostList.vue" + + + +``` diff --git a/.agents/skills/data-client-vue/references/_useLive.md b/.agents/skills/data-client-vue/references/_useLive.md deleted file mode 120000 index eb946942cad6..000000000000 --- a/.agents/skills/data-client-vue/references/_useLive.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/shared/_useLive.mdx \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/_useLive.md b/.agents/skills/data-client-vue/references/_useLive.md new file mode 100644 index 000000000000..74e89472eafc --- /dev/null +++ b/.agents/skills/data-client-vue/references/_useLive.md @@ -0,0 +1,57 @@ + + +```typescript title="Ticker" {32} +import { Entity, RestEndpoint } from '@data-client/rest'; + +export class Ticker extends Entity { + product_id = ''; + trade_id = 0; + price = 0; + size = '0'; + time = Temporal.Instant.fromEpochMilliseconds(0); + bid = '0'; + ask = '0'; + volume = ''; + + pk(): string { + return this.product_id; + } + static key = 'Ticker'; + + static schema = { + price: Number, + time: Temporal.Instant.from, + }; +} + +export const getTicker = new RestEndpoint({ + urlPrefix: 'https://api.exchange.coinbase.com', + path: '/products/:productId/ticker', + schema: Ticker, + process(value, { productId }) { + value.product_id = productId; + return value; + }, + pollFrequency: 2000, +}); +``` + +```html title="AssetPrice.vue" {6} + + + +``` diff --git a/.agents/skills/data-client-vue/references/_useLoading.md b/.agents/skills/data-client-vue/references/_useLoading.md deleted file mode 120000 index 9ed6b996c6bc..000000000000 --- a/.agents/skills/data-client-vue/references/_useLoading.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/shared/_useLoading.mdx \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/_useLoading.md b/.agents/skills/data-client-vue/references/_useLoading.md new file mode 100644 index 000000000000..aea12d095d10 --- /dev/null +++ b/.agents/skills/data-client-vue/references/_useLoading.md @@ -0,0 +1,123 @@ + + +```ts title="PostResource" +import { Entity, resource } from '@data-client/rest'; + +export class Post extends Entity { + id = 0; + author = 0; + title = ''; + body = ''; + votes = 0; + + static key = 'Post'; + + get img() { + return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`; + } +} +export const PostResource = resource({ + path: '/posts/:id', + schema: Post, +}); +``` + +```html title="PostDetail.vue" + + + +``` + +```html title="PostForm.vue" + + + +``` + +```html title="PostCreate.vue" {9-14} + + + +``` + +```html title="Navigation.vue" + + + +``` diff --git a/.agents/skills/data-client-vue/references/data-dependency.md b/.agents/skills/data-client-vue/references/data-dependency.md deleted file mode 120000 index 1613833f6d57..000000000000 --- a/.agents/skills/data-client-vue/references/data-dependency.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/getting-started/data-dependency.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/data-dependency.md b/.agents/skills/data-client-vue/references/data-dependency.md new file mode 100644 index 000000000000..bf42ed3c9ea1 --- /dev/null +++ b/.agents/skills/data-client-vue/references/data-dependency.md @@ -0,0 +1,364 @@ + + +# Rendering Asynchronous Data + +Make your components reusable by binding the data where you **use** it with the one-line [useSuspense()](./useSuspense.md), +which guarantees data with [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await). + +```ts title="Resources" +import { Entity, resource } from '@data-client/rest'; + +export class User extends Entity { + id = 0; + name = ''; + username = ''; + email = ''; + phone = ''; + website = ''; + + get profileImage() { + return `https://i.pravatar.cc/64?img=${this.id + 4}`; + } + + static key = 'User'; +} +export const UserResource = resource({ + urlPrefix: 'https://jsonplaceholder.typicode.com', + path: '/users/:id', + schema: User, +}); + +export class Post extends Entity { + id = 0; + author = User.fromJS(); + title = ''; + body = ''; + + static key = 'Post'; + + static schema = { + author: User, + }; +} +export const PostResource = resource({ + path: '/posts/:id', + schema: Post, + paginationField: 'page', +}); +``` + +```html title="PostDetail.vue" {7} + + + +``` + +```html title="PostItem.vue" + + + +``` + +```html title="PostList.vue" {7} + + + +``` + +```html title="LoadMore.vue" + + + +``` + +```html title="Navigation.vue" + + + +``` + +[](https://react.dev/learn/passing-data-deeply-with-context) + +Do not [prop drill](https://react.dev/learn/passing-data-deeply-with-context#the-problem-with-passing-props). Instead, [useSuspense()](./useSuspense.md) in the components that render the data from it. This is +known as _data co-location_. + +Do not hide data binding hooks inside custom hooks. Instead, put tightly coupled data transformations +in [Query](https://dataclient.io/rest/api/Query) — data logic belongs with the data model, where it stays visible, reusable, +and free to change independently of the view. + +Instead of writing complex update functions or invalidations cascades, Reactive Data Client automatically updates +bound components immediately upon [data change](./mutations.md). This is known as _reactive programming_. + +## Loading and Error {#async-fallbacks} + +You might have noticed the return type shows the value is always there. [useSuspense()](./useSuspense.md) operates very much +with [await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await). This enables +us to make error/loading disjoint from data usage. + +### Async Boundaries {#boundaries} + +Instead we place Vue's built-in [\](https://vuejs.org/guide/built-ins/suspense.html) along with [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured) to handling loading and error conditions at or above navigational boundaries like **pages, +routes, or [modals](https://www.appcues.com/blog/modal-dialog-windows)**. + +```html title="Dashboard.vue" {13-20} + + + +``` + +Centralizing fallbacks this way eliminates redundant loading indicators while keeping components reusable. +The loading fallback is customized with the `#fallback` slot of [\](https://vuejs.org/guide/built-ins/suspense.html#loading-state), +and the error fallback by rendering what you choose from [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured). + +### Stateful + +You may find cases where it's still useful to use a stateful approach to fallbacks. +For these cases, or compatibility with some component libraries, [useDLE()](./useDLE.md) - \[D]ata \[L]oading \[E]rror - is provided. + +```typescript title="ProfileResource" +import { Entity, resource } from '@data-client/rest'; + +export class Profile extends Entity { + id: number | undefined = undefined; + avatar = ''; + fullName = ''; + bio = ''; + + static key = 'Profile'; +} + +export const ProfileResource = resource({ + path: '/profiles/:id', + schema: Profile, +}); +``` + +```html title="ProfileList.vue" {5} + + + +``` + +Since [useDLE](./useDLE.md) does not [useSuspense](./useSuspense.md), you won't be able to easily centrally +orchestrate loading and error code. + +## Conditional + +> **Tip: Conditional Dependencies** +> +> Use `null` as the second argument to any Data Client hook means "do nothing." +> +> ```typescript +> // todo could be undefined if id is undefined +> const todo = await useSuspense( +> TodoResource.get, +> computed(() => (id.value ? { id: id.value } : null)), +> ); +> ``` + +## Subscriptions + +When data is likely to change due to external factor; [useSubscription()](./useSubscription.md) +ensures continual updates while a component is mounted. [useLive()](./useLive.md) calls both +[useSubscription()](./useSubscription.md) and [useSuspense()](./useSuspense.md), making it quite +easy to use fresh data. + +```typescript title="Ticker" {32} +import { Entity, RestEndpoint } from '@data-client/rest'; + +export class Ticker extends Entity { + product_id = ''; + trade_id = 0; + price = 0; + size = '0'; + time = Temporal.Instant.fromEpochMilliseconds(0); + bid = '0'; + ask = '0'; + volume = ''; + + pk(): string { + return this.product_id; + } + static key = 'Ticker'; + + static schema = { + price: Number, + time: Temporal.Instant.from, + }; +} + +export const getTicker = new RestEndpoint({ + urlPrefix: 'https://api.exchange.coinbase.com', + path: '/products/:productId/ticker', + schema: Ticker, + process(value, { productId }) { + value.product_id = productId; + return value; + }, + pollFrequency: 2000, +}); +``` + +```html title="AssetPrice.vue" {6} + + + +``` + +Subscriptions are orchestrated by [Managers](https://dataclient.io/vue/api/Manager). Out of the box, +polling based subscriptions can be used by adding [pollFrequency](https://dataclient.io/rest/api/Endpoint#pollfrequency) to an Endpoint or Resource. +For pushed based networking protocols like SSE and websockets, see the [example stream manager](https://dataclient.io/vue/concepts/managers#data-stream). + +```typescript +export const getTicker = new RestEndpoint({ + urlPrefix: 'https://api.exchange.coinbase.com', + path: '/products/:productId/ticker', + schema: Ticker, + pollFrequency: 2000, +}); +``` diff --git a/.agents/skills/data-client-vue/references/devtools-debugging.md b/.agents/skills/data-client-vue/references/devtools-debugging.md deleted file mode 120000 index d75fb0dae66f..000000000000 --- a/.agents/skills/data-client-vue/references/devtools-debugging.md +++ /dev/null @@ -1 +0,0 @@ -../../data-client-react/references/devtools-debugging.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/devtools-debugging.md b/.agents/skills/data-client-vue/references/devtools-debugging.md new file mode 100644 index 000000000000..a6551fccbb53 --- /dev/null +++ b/.agents/skills/data-client-vue/references/devtools-debugging.md @@ -0,0 +1,366 @@ +# Debugging Data Client with Chrome DevTools MCP + +Debug `@data-client/react` or `@data-client/vue` state and actions programmatically via Chrome DevTools MCP `evaluate_script`. The app's `DevToolsManager` exposes the Controller on `globalThis.__DC_CONTROLLERS__` (a `Map` keyed by `devtoolsName`) in dev mode. + +## Prerequisites + +1. Dev server running with `NODE_ENV !== 'production'` +2. Chrome DevTools MCP connected and page loaded +3. `DevToolsManager` included in `DataProvider` (React) or `DataClientPlugin` (Vue) managers (default in dev mode) + +## Step 1: Access the Controller + +`DevToolsManager` registers controllers in `globalThis.__DC_CONTROLLERS__` keyed by +`devtoolsName` — defaults to `"Data Client: "`. + +### Discover available controllers + +```js +// evaluate_script +() => { + const m = globalThis.__DC_CONTROLLERS__; + if (!m || m.size === 0) return 'no controllers registered'; + return [...m.keys()]; +} +``` + +### Get a controller by key + +Use the key from discovery. Always use `.get(devtoolsName)` with the actual key — not `.values().next().value` — so you target the correct store when multiple `DataProvider`s or Vue apps exist. + +```js +// evaluate_script +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); + if (!ctrl) return 'not found'; + return { ok: true, stateKeys: Object.keys(ctrl.getState()) }; +} +``` + +## Step 2: Install the Debug Shim + +Run this **once** after the page loads. It wraps dispatch to capture all actions in a circular buffer. + +```js +// evaluate_script +(() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + if (!ctrl) return { error: 'No controller found' }; + + globalThis.__DC_ACTION_LOG__ = []; + const MAX_LOG = 200; + + const origDispatch = ctrl._dispatch.bind(ctrl); + ctrl._dispatch = (action) => { + const entry = { + type: action.type, + key: action.key, + ts: Date.now(), + }; + + if (action.endpoint) entry.endpoint = action.endpoint.name || action.endpoint.key; + if (action.args) entry.args = JSON.parse(JSON.stringify(action.args)); + if (action.meta?.date) entry.date = action.meta.date; + if (action.error) entry.error = true; + + globalThis.__DC_ACTION_LOG__.push(entry); + if (globalThis.__DC_ACTION_LOG__.length > MAX_LOG) { + globalThis.__DC_ACTION_LOG__ = globalThis.__DC_ACTION_LOG__.slice(-MAX_LOG / 2); + } + + return origDispatch(action); + }; + + return { ok: true, stateKeys: Object.keys(ctrl.getState()) }; +})() +``` + +## Step 3: Read State + +### High-level queries (denormalized, schema-aware) + +Controller provides `getResponse`, `getError`, and `get` that denormalize through schemas — pass `ctrl.getState()` as the last argument. + +```js +// evaluate_script — get denormalized response for an endpoint +async () => { + const mod = await import('/src/resources/Todo.ts'); + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + const state = ctrl.getState(); + const { data, expiryStatus, expiresAt } = ctrl.getResponse( + mod.TodoResource.getList, + {}, + state, + ); + return { data, expiryStatus, expiresAt }; +} +``` + +```js +// evaluate_script — check if an endpoint has an error +async () => { + const mod = await import('/src/resources/Todo.ts'); + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + const state = ctrl.getState(); + const error = ctrl.getError( + mod.TodoResource.get, + { id: '5' }, + state, + ); + return { error: error?.message ?? null }; +} +``` + +```js +// evaluate_script — query a Queryable schema (Entity, Collection, Query) +async () => { + const mod = await import('/src/resources/Todo.ts'); + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + const state = ctrl.getState(); + const result = ctrl.get(mod.Todo, { id: '5' }, state); + return result; +} +``` + +### Raw normalized state inspection + +Use these when you need to see the raw cache structure without denormalization. + +```js +// evaluate_script — state overview +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + const state = ctrl.getState(); + return { + entityTypes: Object.keys(state.entities), + endpointCount: Object.keys(state.endpoints).length, + metaCount: Object.keys(state.meta).length, + optimisticCount: state.optimistic.length, + lastReset: state.lastReset, + }; +} +``` + +```js +// evaluate_script — inspect specific entity type by key +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + const entities = ctrl.getState().entities['Todo']; + if (!entities) return { error: 'Entity not found' }; + const pks = Object.keys(entities); + return { + count: pks.length, + samplePKs: pks.slice(0, 10), + sample: pks.length > 0 ? entities[pks[0]] : null, + }; +} +``` + +```js +// evaluate_script — find endpoint cache entries by path substring +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + const state = ctrl.getState(); + const keys = Object.keys(state.endpoints).filter(k => k.includes('/todos')); + return keys.map(k => ({ + key: k, + value: state.endpoints[k], + meta: state.meta[k], + })); +} +``` + +### Inspect a specific entity by pk + +```js +// evaluate_script +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + return ctrl.getState().entities?.['Todo']?.['5']; +} +``` + +### Check endpoint metadata (expiry, errors, invalidation) + +```js +// evaluate_script +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + const state = ctrl.getState(); + const key = Object.keys(state.meta).find(k => k.includes('/todos')); + return key ? { key, ...state.meta[key] } : 'no meta found'; +} +``` + +## Step 4: Track Actions + +After installing the debug shim (Step 2): + +### Read recent actions + +```js +// evaluate_script +() => globalThis.__DC_ACTION_LOG__?.slice(-20) ?? [] +``` + +### Filter by action type + +```js +// evaluate_script — track only fetches +() => (globalThis.__DC_ACTION_LOG__ ?? []) + .filter(a => a.type === 'rdc/fetch' || a.type === 'rdc/setresponse') + .slice(-20) +``` + +### Filter errors + +```js +// evaluate_script +() => (globalThis.__DC_ACTION_LOG__ ?? []).filter(a => a.error) +``` + +### Clear action log + +```js +// evaluate_script +() => { globalThis.__DC_ACTION_LOG__ = []; return { cleared: true }; } +``` + +## Step 5: Mutate State via Controller + +Use Controller methods — **never** dispatch raw actions. + +### Invalidate an endpoint (force refetch) + +```js +// evaluate_script — triggers refetch for subscribed components +async () => { + const mod = await import('/src/resources/Todo.ts'); + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + await ctrl.invalidate(mod.TodoResource.get, { id: '5' }); + return { invalidated: true }; +} +``` + +### Invalidate endpoints matching a pattern + +```js +// evaluate_script +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + ctrl.invalidateAll({ testKey: key => key.includes('/todos') }); + return 'todo endpoints invalidated'; +} +``` + +### Expire endpoints (mark stale, refetch on next use) + +```js +// evaluate_script +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + ctrl.expireAll({ testKey: key => key.includes('/todos') }); + return 'todo endpoints expired'; +} +``` + +### Set a value directly + +```js +// evaluate_script — use setResponse to inject mock data +async () => { + const mod = await import('/src/resources/Todo.ts'); + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + await ctrl.setResponse( + mod.TodoResource.getList, + {}, + [{ id: 1, title: 'Mock Todo', completed: false }], + ); + return { set: true }; +} +``` + +### Reset entire store + +```js +// evaluate_script +() => { + const ctrl = globalThis.__DC_CONTROLLERS__?.values().next().value; + ctrl.resetEntireStore(); + return { reset: true }; +} +``` + +## Correlating with Network Requests + +Use `list_network_requests` with `resourceTypes: ["fetch", "xhr"]` to see API calls, +then cross-reference with endpoint keys in state. + +## Action Types Reference + +| Type | Controller Method | Description | +|---|---|---| +| `rdc/fetch` | `fetch()` | Network request initiated | +| `rdc/setresponse` | `setResponse()` | Response written to cache | +| `rdc/set` | `set()` | Direct entity value set | +| `rdc/optimistic` | (automatic) | Optimistic update applied | +| `rdc/invalidate` | `invalidate()` | Single endpoint invalidated | +| `rdc/invalidateall` | `invalidateAll()` | Bulk invalidation by key test | +| `rdc/expireall` | `expireAll()` | Bulk mark-stale by key test | +| `rdc/reset` | `resetEntireStore()` | Full store reset | +| `rdc/subscribe` | `subscribe()` | Subscription registered | +| `rdc/unsubscribe` | `unsubscribe()` | Subscription removed | +| `rdc/gc` | (automatic) | Garbage collection | + +## Controller State Readers + +All take `state` (from `ctrl.getState()`) as the **last** argument. + +| Method | Signature | Returns | +|---|---|---| +| `getResponse` | `(endpoint, ...args, state)` | `{ data, expiryStatus, expiresAt }` — denormalized through schema | +| `getError` | `(endpoint, ...args, state)` | `ErrorTypes \| undefined` | +| `get` | `(schema, ...args, state)` | `Denormalized \| undefined` for any Queryable schema | +| `getQueryMeta` | `(schema, ...args, state)` | `{ data, countRef }` | + +`expiryStatus` values: `1` = Invalid, `2` = InvalidIfStale, `3` = Valid. + +## State Shape Reference + +```ts +State = { + entities: { [entityKey: string]: { [pk: string]: EntityInstance } }, + endpoints: { [cacheKey: string]: PK | PK[] | unknown }, + indexes: { [entityKey: string]: { [indexName: string]: { [lookupValue: string]: PK } } }, + meta: { + [key: string]: { + date, fetchedAt, expiresAt, + prevExpiresAt?, error?, invalidated?, errorPolicy?: 'hard' | 'soft' + } + }, + entitiesMeta: { [entityKey: string]: { [pk: string]: { date, expiresAt, fetchedAt } } }, + optimistic: (SetResponseAction | OptimisticAction)[], + lastReset: number, +} +``` + +## Polling Pattern + +For monitoring ongoing activity, poll with short intervals: + +1. Install shim (Step 2) +2. Trigger the user action or navigation +3. Wait 2–3 seconds +4. Read actions (Step 4) — check for `rdc/fetch` then `rdc/setresponse` pairs +5. If needed, read entity state (Step 3) to verify cache contents +6. Repeat if watching for subscription updates + +## Debugging Checklist + +1. **Verify controller exists**: Check `__DC_CONTROLLERS__` map size +2. **Inspect state shape**: Get entity types and endpoint count +3. **Check specific data**: Look up entities by type and pk +4. **Review endpoint metadata**: Check expiry, errors, invalidation status +5. **Track actions**: Read the action log for recent dispatches +6. **Correlate network**: Compare `list_network_requests` with endpoint keys +7. **Force refresh**: Use `invalidateAll` or `expireAll` to trigger refetches diff --git a/.agents/skills/data-client-vue/references/getDefaultManagers.md b/.agents/skills/data-client-vue/references/getDefaultManagers.md deleted file mode 120000 index 8cb0cf50caab..000000000000 --- a/.agents/skills/data-client-vue/references/getDefaultManagers.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/getDefaultManagers.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/getDefaultManagers.md b/.agents/skills/data-client-vue/references/getDefaultManagers.md new file mode 100644 index 000000000000..79cc0d6e9040 --- /dev/null +++ b/.agents/skills/data-client-vue/references/getDefaultManagers.md @@ -0,0 +1,112 @@ + + +# getDefaultManagers() + +`getDefaultManagers` returns an Array of [Managers](https://dataclient.io/vue/api/Manager) to be sent to [DataClientPlugin](./installation.md#add-provider-at-top-level-component). + +This makes it simple to configure and add custom [Managers](https://dataclient.io/vue/api/Manager), while remaining robust against +any potential changes to the default managers. + +Currently returns \[[DevToolsManager](https://dataclient.io/vue/api/DevToolsManager)\*, [NetworkManager](https://dataclient.io/vue/api/NetworkManager), [SubscriptionManager](https://dataclient.io/vue/api/SubscriptionManager)]. + +\*(`DevToolsManager` is excluded in production builds.) + +## Usage + +```ts title="main.ts" +import { createApp } from 'vue'; +import { DataClientPlugin, getDefaultManagers } from '@data-client/vue'; +import App from './App.vue'; + +const managers = getDefaultManagers({ + // set fallback expiry time to an hour + networkManager: { dataExpiryLength: 1000 * 60 * 60 }, +}); + +const app = createApp(App); +app.use(DataClientPlugin, { managers }); +app.mount('#app'); +``` + +When `managers` is omitted, `DataClientPlugin` uses `getDefaultManagers()` with no arguments. +See [installation](./installation.md#add-provider-at-top-level-component) for the +other `DataClientPlugin` options. + +## Arguments + +Each argument represents a configuration of the manager. It can be of three possible types: + +- Any plain object is used as options to be sent to the manager's constructor. +- An instance of the manager to be used directly. +- `null`. When sent will exclude the manager. + +```ts +getDefaultManagers({ + devToolsManager: { trace: true }, + networkManager: new NetworkManager({ errorExpiryLength: 1 }), + subscriptionManager: null, +}); +``` + +### networkManager + +> **Note** +> +> `null` is not allowed here since NetworkManager is required + +`dataExpiryLength` is used as a fallback when an Endpoint does not have [dataExpiryLength](https://dataclient.io/docs/concepts/expiry-policy#endpointdataexpirylength) defined. + +`errorExpiryLength` is used as a fallback when an Endpoint does not have [errorExpiryLength](https://dataclient.io/docs/concepts/expiry-policy#endpointerrorexpirylength) defined. + +### devToolsManager + +[Arguments](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md) +to send to redux devtools. + +### subscriptionManager + +A class that implements `SubscriptionConstructable` like [PollingSubscription](https://dataclient.io/vue/api/PollingSubscription) + +## Examples + +### Tracing actions + +For example, we can enable the [trace](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#trace) option to help track down where actions are dispatched from. This has a large performance impact, so it is normally disabled. + +```ts +const managers = getDefaultManagers({ + devToolsManager: { trace: true }, +}); +``` + +### Manager inheritance + +Sending manager instances allows us to customize managers using inheritance. + +```ts +import { NetworkManager, type FetchAction } from '@data-client/vue'; + +class LoggingNetworkManager extends NetworkManager { + protected handleFetch(action: FetchAction) { + console.log('fetching', action.key); + return super.handleFetch(action); + } +} + +const managers = getDefaultManagers({ + networkManager: new LoggingNetworkManager(), +}); +``` + +### Disabling + +Using `null` will remove managers completely. [NetworkManager](https://dataclient.io/vue/api/NetworkManager) cannot be removed this way. + +```ts +const managers = getDefaultManagers({ + devToolsManager: null, + subscriptionManager: null, +}); +``` + +Here we disable every manager except [NetworkManager](https://dataclient.io/vue/api/NetworkManager). diff --git a/.agents/skills/data-client-vue/references/installation.md b/.agents/skills/data-client-vue/references/installation.md deleted file mode 120000 index 7fca195ef422..000000000000 --- a/.agents/skills/data-client-vue/references/installation.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/getting-started/installation.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/installation.md b/.agents/skills/data-client-vue/references/installation.md new file mode 100644 index 000000000000..bc39ca65a263 --- /dev/null +++ b/.agents/skills/data-client-vue/references/installation.md @@ -0,0 +1,79 @@ + + +# Getting Started with Reactive Data Client + +> **Tip: Use Agent Skills** +> +> Prefer to scaffold via your AI agent? See [Agent Skills](https://dataclient.io/vue/getting-started/agent-skills) and run `/data-client-setup`. + +## Install the plugin {#add-provider-at-top-level-component} + +Install the [Vue plugin](https://vuejs.org/guide/reusability/plugins.html) when creating your app. + +```bash +npm install @data-client/vue @data-client/rest +``` + +```tsx title="main.ts" +import { createApp } from 'vue'; +import { DataClientPlugin } from '@data-client/vue'; + +const app = createApp(App); + +app.use(DataClientPlugin, { + // optional overrides + // managers: getDefaultManagers(), + // initialState, + // Controller, + // gcPolicy, +}); + +app.mount('#app'); +``` + +[Next: Define Data »](https://dataclient.io/vue/getting-started/resource) + +## Example + +Example app: [vue-todo-app](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app) ([`src/main.ts`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/main.ts), [`src/pages/UserTodos.vue`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/pages/UserTodos.vue)) + +## Supported Tools + +
+ +TypeScript 4.0+ + +TypeScript is optional, but requires at least version [4.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#variadic-tuple-types) and [strictNullChecks](https://www.typescriptlang.org/tsconfig#strictNullChecks) for full type enforcement. + +
+ +
+ +Older browser support + +If your application targets older browsers (a few years or more), be sure to load polyfills. +Typically this is done with [@babel/preset-env useBuiltIns: 'entry'](https://babeljs.io/docs/en/babel-preset-env#usebuiltins), +coupled with importing [core-js](https://www.npmjs.com/package/core-js) at the entrypoint of your application. + +This ensures only the needed polyfills for your browser support targets are included in your application bundle. + +For instance `TypeError: Object.hasOwn is not a function` + +
+ +
+ +Internet Explorer support + +If you see `Uncaught TypeError: Class constructor Resource cannot be invoked without 'new'`, +follow the instructions to [add legacy browser support to packages](https://dataclient.io/vue/guides/legacy-browser) + +
+ +
+ +Vue 3 + +`@data-client/vue` supports Vue 3 and is built on the [Composition API](https://vuejs.org/guide/extras/composition-api-faq.html). + +
diff --git a/.agents/skills/data-client-vue/references/mutations.md b/.agents/skills/data-client-vue/references/mutations.md deleted file mode 120000 index aaa624e6eb92..000000000000 --- a/.agents/skills/data-client-vue/references/mutations.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/getting-started/mutations.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/mutations.md b/.agents/skills/data-client-vue/references/mutations.md new file mode 100644 index 000000000000..edc5f7b3b1b5 --- /dev/null +++ b/.agents/skills/data-client-vue/references/mutations.md @@ -0,0 +1,376 @@ + + +# Data mutations + +Using our [Create, Update, and Delete](https://dataclient.io/vue/concepts/atomic-mutations) endpoints with +[Controller.fetch()](./Controller.md#fetch) reactively updates _all_ appropriate components atomically (at the same time). + +[useController()](./useController.md) gives components access to this global supercharged [setState()](https://react.dev/reference/react/useState#setstate). + +[//]: # "TODO: Add create, and delete examples as well (in tabs)" + +```ts title="TodoResource" +import { Entity, resource } from '@data-client/rest'; + +export class Todo extends Entity { + id = 0; + userId = 0; + title = ''; + completed = false; + + static key = 'Todo'; +} +export const TodoResource = resource({ + urlPrefix: 'https://jsonplaceholder.typicode.com', + path: '/todos/:id', + searchParams: {} as { userId?: string | number } | undefined, + schema: Todo, + optimistic: true, +}); +``` + +```html title="TodoItem.vue" {8-12,14-16} + + + +``` + +```html title="CreateTodo.vue" {10-13} + + + +``` + +```html title="TodoList.vue" + + + +``` + +Rather than triggering invalidation cascades or using manually written update functions, +Data Client reactively updates appropriate components using the fetch response. + +## Optimistic mutations based on previous state {#optimistic-updates} + +```ts title="Post" +import { Entity, schema } from '@data-client/rest'; + +export class Post extends Entity { + id = 0; + author = { id: 0 }; + title = ''; + body = ''; + votes = 0; + + static key = 'Post'; + + static schema = { + author: EntityMixin( + class User { + id = 0; + }, + ), + }; + + get img() { + return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`; + } +} +``` + +```ts title="PostResource" {15-22} +import { resource } from '@data-client/rest'; +import { Post } from './Post'; + +export { Post }; + +export const PostResource = resource({ + path: '/posts/:id', + searchParams: {} as { userId?: string | number } | undefined, + schema: Post, +}).extend('vote', { + path: '/posts/:id/vote', + method: 'POST', + body: undefined, + schema: Post, + getOptimisticResponse(snapshot, { id }) { + const post = snapshot.get(Post, { id }); + if (!post) throw snapshot.abort; + return { + id, + votes: post.votes + 1, + }; + }, +}); +``` + +```html title="PostItem.vue" {9} + + + +``` + +```html title="TotalVotes.vue" {13} + + + +``` + +```html title="PostList.vue" + + + +``` + +[getOptimisticResponse](https://dataclient.io/rest/guides/optimistic-updates) is just like [setState with an updater function](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state). [Snapshot](https://dataclient.io/vue/api/Snapshot) provides typesafe access to the previous store value, +which we use to return the _expected_ fetch response. + +Reactive Data Client ensures [data integrity against any possible networking failure or race condition](https://dataclient.io/rest/guides/optimistic-updates#optimistic-transforms), so don't +worry about network failures, multiple mutation calls editing the same data, or other common +problems in asynchronous programming. + +## Tracking mutation loading + +[useLoading()](./useLoading.md) enhances async functions by tracking their loading and error states. + +```ts title="PostResource" +import { Entity, resource } from '@data-client/rest'; + +export class Post extends Entity { + id = 0; + author = 0; + title = ''; + body = ''; + votes = 0; + + static key = 'Post'; + + get img() { + return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`; + } +} +export const PostResource = resource({ + path: '/posts/:id', + schema: Post, +}); +``` + +```html title="PostDetail.vue" + + + +``` + +```html title="PostForm.vue" + + + +``` + +```html title="PostCreate.vue" {9-14} + + + +``` + +```html title="Navigation.vue" + + + +``` diff --git a/.agents/skills/data-client-vue/references/useCache.md b/.agents/skills/data-client-vue/references/useCache.md deleted file mode 120000 index 3659e1fe1516..000000000000 --- a/.agents/skills/data-client-vue/references/useCache.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useCache.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useCache.md b/.agents/skills/data-client-vue/references/useCache.md new file mode 100644 index 000000000000..28a29d8000a4 --- /dev/null +++ b/.agents/skills/data-client-vue/references/useCache.md @@ -0,0 +1,130 @@ + + +# useCache() + +Data rendering without the fetch. + +Access any [Endpoint](https://dataclient.io/rest/api/Endpoint)'s response. If the response does not exist, returns +`undefined`. This can be used to check for an `Endpoint's` existance like for authentication. + +`useCache()` is reactive to data [mutations](./mutations.md); rerendering only when necessary. + +## Usage + +```ts title="UserResource" +import { Entity, resource } from '@data-client/rest'; + +export class User extends Entity { + id = ''; + name = ''; + isAdmin = false; + + static key = 'User'; +} +export const UserResource = resource({ + path: '/users/:id', + schema: User, +}).extend('current', { + path: '/user', + schema: User, +}); +``` + +```html title="Unauthed.vue" + + + +``` + +```html title="Authorized.vue" + + + +``` + +```html title="AuthorizedPage.vue" + + + +``` + +See [truthiness narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing) for +more information about type handling + +## Behavior + +`useCache()` returns a [ComputedRef](https://vuejs.org/api/reactivity-core.html#computed). The table +below describes its `.value`. + +| Expiry Status | Returns | Conditions | +| ------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Invalid | `undefined` | not in store, [deletion](https://dataclient.io/rest/api/resource#delete), [invalidation](./Controller.md#invalidate), [invalidIfStale](https://dataclient.io/vue/concepts/expiry-policy#endpointinvalidifstale) | +| Stale | denormalized | (first-render, arg change) & [expiry < now](https://dataclient.io/vue/concepts/expiry-policy) | +| Valid | denormalized | fetch completion | +| | `undefined` | `null` used as second argument | + +> **Tip: Conditional Dependencies** +> +> Use `null` as the second argument to any Data Client hook means "do nothing." +> +> ```typescript +> // todo could be undefined if id is undefined +> const todo = useCache( +> TodoResource.get, +> computed(() => (id.value ? { id: id.value } : null)), +> ); +> ``` + +## Types + +```typescript +function useCache( + endpoint: ReadEndpoint, + ...args: MaybeRefsOrGetters> | [null] +): ComputedRef>; +``` + +Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter +functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't +follow prop or route changes, so use a getter or `computed` when an argument can change. + +The result updates when the arguments change. diff --git a/.agents/skills/data-client-vue/references/useController.md b/.agents/skills/data-client-vue/references/useController.md deleted file mode 120000 index 68fa2a5921fb..000000000000 --- a/.agents/skills/data-client-vue/references/useController.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useController.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useController.md b/.agents/skills/data-client-vue/references/useController.md new file mode 100644 index 000000000000..6ad0b73f00d9 --- /dev/null +++ b/.agents/skills/data-client-vue/references/useController.md @@ -0,0 +1,170 @@ + + +# useController() + +[Controller](./Controller.md) provides type-safe methods to access and dispatch actions to the store. + +For instance [fetch](./Controller.md#fetch), [invalidate](./Controller.md#invalidate), +and [setResponse](./Controller.md#setResponse) + +```html + +``` + +`useController()` must be called inside ` + + +``` + +### Direct entity update + +Use [set](./Controller.md#set) for immediate updates without network requests. Supports functional updates to avoid race conditions. + +```html title="VoteButton.vue" + + + +``` + +### Invalidate after mutation + +Force refetch of related data using [invalidate](./Controller.md#invalidate) or [expireAll](./Controller.md#expireAll). + +```html title="ClearUserCache.vue" + + + +``` + +> **Tip** +> +> For better performance and consistency, prefer [including side effect updates in mutation responses](https://dataclient.io/rest/guides/side-effects). + +### Prefetching + +Use [fetchIfStale](./Controller.md#fetchIfStale) to prefetch without overfetching fresh data. + +```html title="ArticleLink.vue" + + + +``` + +### Websocket updates + +Populate cache with external data via [set](./Controller.md#set). + +```ts title="useWebsocket.ts" +import { onMounted, onUnmounted } from 'vue'; +import { useController } from '@data-client/vue'; + +export function useWebsocket(url: string) { + const ctrl = useController(); + let ws: WebSocket; + + onMounted(() => { + ws = new WebSocket(url); + ws.onmessage = event => { + const { entity, args, data } = JSON.parse(event.data); + ctrl.set(EntityMap[entity], args, data); + }; + }); + onUnmounted(() => ws?.close()); +} +``` + +> **Warning** +> +> For production use, implement a [Manager for data streams](https://dataclient.io/vue/concepts/managers#data-stream) rather than component-level lifecycle hooks. Managers handle connection lifecycle globally and work with SSR. + +### Todo App + +Example app: [vue-todo-app](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app) ([`src/resources/TodoResource.ts`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/resources/TodoResource.ts), [`src/components/TodoItem.vue`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/components/TodoItem.vue)) diff --git a/.agents/skills/data-client-vue/references/useDLE.md b/.agents/skills/data-client-vue/references/useDLE.md deleted file mode 120000 index fa87e066feb2..000000000000 --- a/.agents/skills/data-client-vue/references/useDLE.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useDLE.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useDLE.md b/.agents/skills/data-client-vue/references/useDLE.md new file mode 100644 index 000000000000..ad20c87ccdf8 --- /dev/null +++ b/.agents/skills/data-client-vue/references/useDLE.md @@ -0,0 +1,270 @@ + + +# useDLE() - \[D]ata \[L]oading \[E]rror + +High performance async data rendering without overfetching. With fetch meta data. + +In case you cannot use [suspense](./data-dependency.md#async-fallbacks), useDLE() is just like [useSuspense()](./useSuspense.md) but returns \[D]ata \[L]oading \[E]rror values. + +`useDLE()` is reactive to data [mutations](./mutations.md); rerendering only when necessary. + +## Usage + +```typescript title="ProfileResource" +import { Entity, resource } from '@data-client/rest'; + +export class Profile extends Entity { + id: number | undefined = undefined; + avatar = ''; + fullName = ''; + bio = ''; + + static key = 'Profile'; +} + +export const ProfileResource = resource({ + path: '/profiles/:id', + schema: Profile, +}); +``` + +```html title="ProfileList.vue" + + + +``` + +## Behavior + +`data`, `loading` and `error` are each a [ComputedRef](https://vuejs.org/api/reactivity-core.html#computed). +Destructure them at the top level of ` + + +``` + +### Conditional + +`null` will avoid binding and fetching data + +```ts title="Resources" +import { Entity, resource } from '@data-client/rest'; + +export class Post extends Entity { + id = 0; + userId = 0; + title = ''; + body = ''; + + static key = 'Post'; +} +export const PostResource = resource({ + path: '/posts/:id', + schema: Post, +}); + +export class User extends Entity { + id = 0; + name = ''; + username = ''; + email = ''; + phone = ''; + website = ''; + + get profileImage() { + return `https://i.pravatar.cc/64?img=${this.id + 4}`; + } + + static key = 'User'; +} +export const UserResource = resource({ + urlPrefix: 'https://jsonplaceholder.typicode.com', + path: '/users/:id', + schema: User, +}); +``` + +```html title="PostWithAuthor.vue" {15-21} + + + +``` + +### Embedded data + +When entities are stored in [nested structures](https://dataclient.io/rest/guides/relational-data#nesting), that structure will remain. + +```typescript title="api/Post" +export class PaginatedPost extends Entity { + id = ''; + title = ''; + content = ''; + + static key = 'PaginatedPost'; +} + +export const getPosts = new RestEndpoint({ + path: '/post', + searchParams: { page: '' }, + schema: { + results: new Collection([PaginatedPost]), + nextPage: '', + lastPage: '', + }, +}); +``` + +```html title="ArticleList.vue" {14} + + + +``` diff --git a/.agents/skills/data-client-vue/references/useDebounce.md b/.agents/skills/data-client-vue/references/useDebounce.md deleted file mode 120000 index f4d37592acfe..000000000000 --- a/.agents/skills/data-client-vue/references/useDebounce.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useDebounce.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useDebounce.md b/.agents/skills/data-client-vue/references/useDebounce.md new file mode 100644 index 000000000000..bcfb7660941c --- /dev/null +++ b/.agents/skills/data-client-vue/references/useDebounce.md @@ -0,0 +1,125 @@ + + +# useDebounce() + +Delays updating the parameters by [debouncing](https://css-tricks.com/debouncing-throttling-explained-examples/). + +Useful to avoid spamming network requests when parameters might change quickly (like a typeahead field). + +> **Tip** +> +> `useDebounce()` returns [refs](https://vuejs.org/api/reactivity-core.html#ref), so the debounced +> value can be passed directly to other composables or components. `isPending` is true from the moment +> the input changes until the debounced value is updated. + +## Usage + +```ts title="IssueQuery" +import { RestEndpoint, Entity, Collection } from '@data-client/rest'; + +export class Issue extends Entity { + number = 0; + repository_url = ''; + labels_url = ''; + html_url = ''; + body = ''; + title = ''; + state: 'open' | 'closed' = 'open'; + locked = false; + comments = 0; + created_at = Temporal.Instant.fromEpochMilliseconds(0); + updated_at = Temporal.Instant.fromEpochMilliseconds(0); + closed_at: Temporal.Instant | null = null; + authorAssociation = 'NONE'; + pullRequest: Record | null = null; + declare draft?: boolean; + + static schema = { + created_at: Temporal.Instant.from, + updated_at: Temporal.Instant.from, + closed_at: Temporal.Instant.from, + }; + + pk() { + return [this.repository_url, this.number].join(','); + } +} + +export const issueQuery = new RestEndpoint({ + urlPrefix: 'https://api.github.com', + path: '/search/issues', + searchParams: {} as { q: string }, + paginationField: 'page', + schema: { + incomplete_results: false, + items: new Collection([Issue]), + total_count: 0, + }, +}); +``` + +```html title="IssueList.vue" + + + +``` + +```html title="SearchIssues.vue" + + + +``` + +## Types + +```typescript +function useDebounce( + value: T | Ref, + delay: number, + updatable?: boolean | Ref, +): [Ref, Ref]; +``` + +`value` and `updatable` can be plain values or [refs](https://vuejs.org/api/reactivity-core.html#ref). +Returns a tuple of `[debouncedValue, isPending]` refs. When `updatable` is `false`, the debounced +value stops updating and `isPending` resets to `false`. diff --git a/.agents/skills/data-client-vue/references/useFetch.md b/.agents/skills/data-client-vue/references/useFetch.md deleted file mode 120000 index b3c249d05780..000000000000 --- a/.agents/skills/data-client-vue/references/useFetch.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useFetch.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useFetch.md b/.agents/skills/data-client-vue/references/useFetch.md new file mode 100644 index 000000000000..b19d1ba8e090 --- /dev/null +++ b/.agents/skills/data-client-vue/references/useFetch.md @@ -0,0 +1,165 @@ + + +# useFetch() + +Fetch an Endpoint if it is not in cache or stale. Returns a [Ref](https://vuejs.org/api/reactivity-core.html#ref) +holding the fetch promise (with a `resolved` flag). A new fetch is triggered when the arguments change or +the data is [invalidated](./Controller.md#invalidate). Use it to start fetches early, then read the data +with [useSuspense()](./useSuspense.md), [useCache()](./useCache.md) or [useDLE()](./useDLE.md). + +## Usage + +### Parallel data loading + +`await useSuspense()` runs sequentially in ` + + +``` + +### Prefetching + +`useFetch()` can also be used standalone to ensure resources are available early in a render tree before they are needed. + +> **Tip** +> +> Use in combination with a data-binding hook ([useCache()](./useCache.md), [useSuspense()](./useSuspense.md), [useDLE()](./useDLE.md), [useLive()](./useLive.md)) +> in another component. + +```html title="MasterPost.vue" + +``` + +## Behavior + +| Expiry Status | Fetch | `.value` | `resolved` | Conditions | +| ------------- | --------------- | ---------------- | ---------- | -------------------------------------------------------------------------------------------------------------------- | +| Invalid | yes1 | pending promise | `false` | not in store, [deletion](https://dataclient.io/rest/api/resource#delete), [invalidation](./Controller.md#invalidate) | +| Stale | yes1 | pending promise | `false` | (first-render, arg change) & [expiry < now](https://dataclient.io/vue/concepts/expiry-policy) | +| Valid | no | resolved promise | `true` | fetch completion | +| Error | no | rejected promise | `true` | fetch failed | +| | no | `undefined` | | `null` used as second argument | + +The returned `Ref` is updated with a new promise whenever a fetch is triggered: on argument change, +[invalidation](./Controller.md#invalidate), or [reset](./Controller.md#resetEntireStore). + +> **Note** +> +> 1. Identical fetches are automatically deduplicated + +> **Tip: Conditional Dependencies** +> +> Use `null` as the second argument to any Data Client hook means "do nothing." +> +> ```typescript +> // todo could be undefined if id is undefined +> const todo = useFetch( +> TodoResource.get, +> computed(() => (id.value ? { id: id.value } : null)), +> ); +> ``` + +## Types + +```typescript +function useFetch( + endpoint: ReadEndpoint, + ...args: MaybeRefsOrGetters> | [null] +): Readonly< + Ref< + | (Promise> & { + resolved: boolean; + }) + | undefined + > +>; +``` + +Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter +functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't +follow prop or route changes, so use a getter or `computed` when an argument can change. + +A new fetch is triggered when the arguments change. + +## Examples + +### Checking fetch status + +Use `promise.resolved` to check whether data is still loading: + +```html title="MasterPost.vue" + +``` diff --git a/.agents/skills/data-client-vue/references/useLive.md b/.agents/skills/data-client-vue/references/useLive.md deleted file mode 120000 index fa4e4d082b41..000000000000 --- a/.agents/skills/data-client-vue/references/useLive.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useLive.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useLive.md b/.agents/skills/data-client-vue/references/useLive.md new file mode 100644 index 000000000000..1ea1a4682de2 --- /dev/null +++ b/.agents/skills/data-client-vue/references/useLive.md @@ -0,0 +1,113 @@ + + +# useLive() + +Async rendering of remotely triggered data mutations. + +[useSuspense()](./useSuspense.md) + [useSubscription()](./useSubscription.md) in one composable. + +`useLive()` is reactive to data [mutations](./mutations.md); rerendering only when necessary. + +## Usage + +```typescript title="Ticker" {32} +import { Entity, RestEndpoint } from '@data-client/rest'; + +export class Ticker extends Entity { + product_id = ''; + trade_id = 0; + price = 0; + size = '0'; + time = Temporal.Instant.fromEpochMilliseconds(0); + bid = '0'; + ask = '0'; + volume = ''; + + pk(): string { + return this.product_id; + } + static key = 'Ticker'; + + static schema = { + price: Number, + time: Temporal.Instant.from, + }; +} + +export const getTicker = new RestEndpoint({ + urlPrefix: 'https://api.exchange.coinbase.com', + path: '/products/:productId/ticker', + schema: Ticker, + process(value, { productId }) { + value.product_id = productId; + return value; + }, + pollFrequency: 2000, +}); +``` + +```html title="AssetPrice.vue" + + + +``` + +Like [useSuspense()](./useSuspense.md), `useLive()` returns a Promise, so it is used with `await` in +` + + +``` + +```html title="PostForm.vue" + + + +``` + +```html title="PostCreate.vue" + + + +``` + +```html title="Navigation.vue" + + + +``` + +Returns the wrapped function along with `loading` and `error` [refs](https://vuejs.org/api/reactivity-core.html#ref). +The wrapped function is stable, so no dependency list is needed: any refs or props it reads are +read at call time. + +## Types + +```typescript +export default function useLoading< + F extends (...args: any) => Promise, +>(func: F): [F, Ref, Ref]; +``` + +`loading` is `true` while the returned promise is pending. If `func` rejects, the rejection is +caught and stored in `error`; it is cleared again on the next call. + +## Examples + +### Todo creation + +Example app: [vue-todo-app](https://github.com/reactive/data-client/tree/master/examples/vue-todo-app) ([`src/components/TodoList.vue`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/components/TodoList.vue), [`src/resources/TodoResource.ts`](https://github.com/reactive/data-client/blob/master/examples/vue-todo-app/src/resources/TodoResource.ts)) diff --git a/.agents/skills/data-client-vue/references/useQuery.md b/.agents/skills/data-client-vue/references/useQuery.md deleted file mode 120000 index 8cbb935c7781..000000000000 --- a/.agents/skills/data-client-vue/references/useQuery.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useQuery.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useQuery.md b/.agents/skills/data-client-vue/references/useQuery.md new file mode 100644 index 000000000000..f75097c8cd6e --- /dev/null +++ b/.agents/skills/data-client-vue/references/useQuery.md @@ -0,0 +1,303 @@ + + +# useQuery() + +Data rendering without the fetch. + +Access any [Queryable Schema](https://dataclient.io/rest/api/schema#queryable)'s store value; like [Entity](https://dataclient.io/rest/api/Entity), [All](https://dataclient.io/rest/api/All), [Collection](https://dataclient.io/rest/api/Collection), [Query](https://dataclient.io/rest/api/Query), +[Union](https://dataclient.io/rest/api/Union), and [Scalar](https://dataclient.io/rest/api/Scalar). [Lazy](https://dataclient.io/rest/api/Lazy) fields also work via their [`.query`](https://dataclient.io/rest/api/Lazy#query) accessor. +If the value does not exist, returns `undefined`. + +`useQuery()` is reactive to data [mutations](./mutations.md); rerendering only when necessary. Returns `undefined` +when data is [Invalid](https://dataclient.io/vue/concepts/expiry-policy#invalid). + +> **Tip** +> +> [Queries](https://dataclient.io/rest/api/Query) are a great companion to efficiently render aggregate computations like those that use [groupBy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/groupBy#browser_compatibility), +> [map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map), [reduce](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce), and [filter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). + +## Usage + +```ts title="Post" +import { Entity, schema } from '@data-client/rest'; + +export class Post extends Entity { + id = 0; + author = { id: 0 }; + title = ''; + body = ''; + votes = 0; + + static key = 'Post'; + + static schema = { + author: EntityMixin( + class User { + id = 0; + }, + ), + }; + + get img() { + return `//loremflickr.com/96/72/kitten,cat?lock=${this.id % 16}`; + } +} +``` + +```ts title="PostResource" {15-22} +import { resource } from '@data-client/rest'; +import { Post } from './Post'; + +export { Post }; + +export const PostResource = resource({ + path: '/posts/:id', + searchParams: {} as { userId?: string | number } | undefined, + schema: Post, +}).extend('vote', { + path: '/posts/:id/vote', + method: 'POST', + body: undefined, + schema: Post, + getOptimisticResponse(snapshot, { id }) { + const post = snapshot.get(Post, { id }); + if (!post) throw snapshot.abort; + return { + id, + votes: post.votes + 1, + }; + }, +}); +``` + +```html title="PostItem.vue" {9} + + + +``` + +```html title="TotalVotes.vue" {13} + + + +``` + +```html title="PostList.vue" + + + +``` + +See [truthiness narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#truthiness-narrowing) for +more information about type handling + +## Types + +```typescript +function useQuery( + schema: S, + ...args: MaybeRefsOrGetters> +): ComputedRef | undefined>; +``` + +Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter +functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't +follow prop or route changes, so use a getter or `computed` when an argument can change. + +The result updates when the arguments change. + +### Queryable + +[Queryable](https://dataclient.io/rest/api/schema#queryable) schemas require an `queryKey()` method that returns something. These include +[Entity](https://dataclient.io/rest/api/Entity), [All](https://dataclient.io/rest/api/All), [Collection](https://dataclient.io/rest/api/Collection), [Query](https://dataclient.io/rest/api/Query), +[Union](https://dataclient.io/rest/api/Union), and [Scalar](https://dataclient.io/rest/api/Scalar). [Lazy](https://dataclient.io/rest/api/Lazy) fields produce a Queryable via their [`.query`](https://dataclient.io/rest/api/Lazy#query) accessor. + +```ts +interface Queryable { + queryKey( + args: readonly any[], + queryKey: (...args: any) => any, + getEntity: GetEntity, + getIndex: GetIndex, + // Must be non-void + ): {}; +} +``` + +## Examples + +### Sorting & Filtering + +[Query](https://dataclient.io/rest/api/Query) provides programmatic access to the Reactive Data Client store. + +```ts title="UserResource" +export class User extends Entity { + id = ''; + name = ''; + isAdmin = false; + + static key = 'User'; +} +export const UserResource = resource({ + path: '/users/:id', + schema: User, +}); +``` + +```html title="UsersPage.vue" {22} + + + +``` + +### Lazy relationships + +[Lazy](https://dataclient.io/rest/api/Lazy) fields keep raw IDs during parent denormalization. Use [`.query`](https://dataclient.io/rest/api/Lazy#query) with `useQuery` to resolve them on demand, +isolating re-renders to only the components that need the related data. + +```ts title="Resources" +export class Building extends Entity { + id = ''; + name = ''; + + static key = 'Building'; +} + +export class Department extends Entity { + id = ''; + name = ''; + buildings: string[] = []; + + static schema = { + buildings: new Lazy([Building]), + }; + static key = 'Department'; +} + +export const DepartmentResource = resource({ + path: '/departments/:id', + schema: Department, +}); +``` + +```html title="BuildingList.vue" {8-11} + + + +``` + +```html title="DepartmentsPage.vue" + + + +``` diff --git a/.agents/skills/data-client-vue/references/useSubscription.md b/.agents/skills/data-client-vue/references/useSubscription.md deleted file mode 120000 index 9035c9f29606..000000000000 --- a/.agents/skills/data-client-vue/references/useSubscription.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useSubscription.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useSubscription.md b/.agents/skills/data-client-vue/references/useSubscription.md new file mode 100644 index 000000000000..9ddb3aad9f0f --- /dev/null +++ b/.agents/skills/data-client-vue/references/useSubscription.md @@ -0,0 +1,111 @@ + + +# useSubscription() + +Great for keeping resources up-to-date with frequent changes. + +When using the default [polling subscriptions](https://dataclient.io/vue/api/PollingSubscription), frequency must be set in +[Endpoint](https://dataclient.io/rest/api/Endpoint), otherwise will have no effect. + +> **Tip** +> +> [useLive()](./useLive.md) is a terser way to use in combination with [useSuspense()](./useSuspense.md), + +## Usage + +```typescript title="api/Price" +import { RestEndpoint, Entity } from '@data-client/rest'; + +export class Price extends Entity { + symbol = ''; + price = '0.0'; + // ... + + pk() { + return this.symbol; + } +} + +export const getPrice = new RestEndpoint({ + urlPrefix: 'http://test.com', + path: '/price/:symbol', + schema: Price, + pollFrequency: 5000, +}); +``` + +```html title="MasterPrice.vue" + +``` + +## Behavior + +> **Tip: Conditional Dependencies** +> +> Use `null` as the second argument to any Data Client hook means "do nothing." +> +> ```typescript +> // todo could be undefined if id is undefined +> const todo = useSubscription( +> TodoResource.get, +> computed(() => (id.value ? { id: id.value } : null)), +> ); +> ``` + +The subscription is created when the component is set up and removed when it unmounts. When +an argument passed as a [ref](https://vuejs.org/api/reactivity-core.html#ref) changes, the previous +subscription is removed and a new one is created for the new arguments. + +## Types + +```typescript +function useSubscription( + endpoint: ReadEndpoint, + ...args: MaybeRefsOrGetters> | [null] +): void; +``` + +Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter +functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't +follow prop or route changes, so use a getter or `computed` when an argument can change. + +## Examples + +### Only subscribe while element is visible + +```html title="MasterPrice.vue" + + + +``` + +When `null` is sent as the second argument, the subscription is deactivated. Of course, +if other components are still subscribed the data updates will still be active. + +[useElementVisibility()](https://vueuse.org/core/useElementVisibility/) from VueUse uses [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API), which is very performant. [Template refs](https://vuejs.org/guide/essentials/template-refs.html) allow +us to access the [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model). diff --git a/.agents/skills/data-client-vue/references/useSuspense.md b/.agents/skills/data-client-vue/references/useSuspense.md deleted file mode 120000 index 4542002302e8..000000000000 --- a/.agents/skills/data-client-vue/references/useSuspense.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/api/useSuspense.md \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/useSuspense.md b/.agents/skills/data-client-vue/references/useSuspense.md new file mode 100644 index 000000000000..bdbf0e3e4995 --- /dev/null +++ b/.agents/skills/data-client-vue/references/useSuspense.md @@ -0,0 +1,418 @@ + + +# useSuspense() + +High performance async data rendering without overfetching. + +[await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) `useSuspense()` in Vue components. This means the remainder of the component only runs after the data has loaded, avoiding the complexity of handling loading and error conditions. Instead, fallback handling is +[centralized](./data-dependency.md#boundaries) with Vue's built-in [Suspense](https://vuejs.org/guide/built-ins/suspense.html). + +`useSuspense()` is reactive to data [mutations](./mutations.md); rerendering only when necessary. + +## Usage + +**Rest** + +```typescript title="ProfileResource" +import { Entity, resource } from '@data-client/rest'; + +export class Profile extends Entity { + id: number | undefined = undefined; + avatar = ''; + fullName = ''; + bio = ''; + + static key = 'Profile'; +} + +export const ProfileResource = resource({ + path: '/profiles/:id', + schema: Profile, +}); +``` + +```html title="ProfileDetail.vue" + + + +``` + +**Promise** + +```typescript title="Profile" +import { Endpoint } from '@data-client/endpoint'; + +export const getProfile = new Endpoint( + (id: number) => + Promise.resolve({ + id, + fullName: 'Jing Chen', + bio: 'Creator of Flux Architecture', + avatar: 'https://avatars.githubusercontent.com/u/5050204?v=4', + }), + { + key(id) { + return `getProfile${id}`; + }, + }, +); +``` + +```html title="ProfileDetail.vue" + + + +``` + +## Behavior + +Cache policy is [Stale-While-Revalidate](https://tools.ietf.org/html/rfc5861) by default but also [configurable](https://dataclient.io/vue/concepts/expiry-policy). + +| Expiry Status | Fetch | Suspend | Error | Conditions | +| ------------- | --------------- | ------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Invalid | yes1 | yes | no | not in store, [deletion](https://dataclient.io/rest/api/resource#delete), [invalidation](./Controller.md#invalidate), [invalidIfStale](https://dataclient.io/vue/concepts/expiry-policy#endpointinvalidifstale) | +| Stale | yes1 | no | no | (first-render, arg change) & [expiry < now](https://dataclient.io/vue/concepts/expiry-policy) | +| Valid | no | no | maybe2 | fetch completion | +| | no | no | no | `null` used as second argument | + +> **Note** +> +> 1. Identical fetches are automatically deduplicated +> 2. [Hard errors](https://dataclient.io/vue/concepts/error-policy#hard) to be [caught](./data-dependency.md#async-fallbacks) by [onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured) + +> **Tip: Conditional Dependencies** +> +> Use `null` as the second argument to any Data Client hook means "do nothing." +> +> ```typescript +> // todo could be undefined if id is undefined +> const todo = await useSuspense( +> TodoResource.get, +> computed(() => (id.value ? { id: id.value } : null)), +> ); +> ``` + +## Types + +```typescript +function useSuspense( + endpoint: ReadEndpoint, + ...args: MaybeRefsOrGetters> | [null] +): Promise>>>; +``` + +Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter +functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't +follow prop or route changes, so use a getter or `computed` when an argument can change. + +The result updates when the arguments change. +While data for new arguments loads, the result keeps the previous data instead of becoming `undefined`. +If that fetch fails, reading the result throws the error (per its [error policy](https://dataclient.io/vue/concepts/error-policy)), so it reaches +[onErrorCaptured()](https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured). + +## Examples + +### List + +```typescript title="ProfileResource" +import { Entity, resource } from '@data-client/rest'; + +export class Profile extends Entity { + id: number | undefined = undefined; + avatar = ''; + fullName = ''; + bio = ''; + + static key = 'Profile'; +} + +export const ProfileResource = resource({ + path: '/profiles/:id', + schema: Profile, +}); +``` + +```html title="ProfileList.vue" + + + +``` + +### Pagination + +Reactive [pagination](https://dataclient.io/rest/guides/pagination) is achieved with [mutable schemas](https://dataclient.io/rest/api/Collection) + +```ts title="User" +import { Entity } from '@data-client/rest'; + +export class User extends Entity { + id = 0; + name = ''; + username = ''; + email = ''; + phone = ''; + website = ''; + + get profileImage() { + return `https://i.pravatar.cc/64?img=${this.id + 4}`; + } + + pk() { + return this.id; + } + static key = 'User'; +} +``` + +```ts title="Post" {22,24} +import { Entity, resource, Collection } from '@data-client/rest'; +import { User } from './User'; + +export class Post extends Entity { + id = 0; + author = User.fromJS(); + title = ''; + body = ''; + + pk() { + return this.id; + } + static key = 'Post'; + + static schema = { + author: User, + }; +} +export const PostResource = resource({ + path: '/posts/:id', + schema: Post, + paginationField: 'cursor', +}).extend('getList', { + schema: { posts: new Collection([Post]), cursor: '' }, +}); +``` + +```html title="PostItem.vue" + + + +``` + +```html title="LoadMore.vue" + + + +``` + +```html title="PostList.vue" + + + +``` + +### Sequential + +When fetch parameters depend on data from another resource. + +```html + +``` + +### Conditional + +`null` will avoid binding and fetching data + +```ts title="Resources" +import { Entity, resource } from '@data-client/rest'; + +export class Post extends Entity { + id = 0; + userId = 0; + title = ''; + body = ''; + + static key = 'Post'; +} +export const PostResource = resource({ + path: '/posts/:id', + schema: Post, +}); + +export class User extends Entity { + id = 0; + name = ''; + username = ''; + email = ''; + phone = ''; + website = ''; + + get profileImage() { + return `https://i.pravatar.cc/64?img=${this.id + 4}`; + } + + static key = 'User'; +} +export const UserResource = resource({ + urlPrefix: 'https://jsonplaceholder.typicode.com', + path: '/users/:id', + schema: User, +}); +``` + +```html title="PostWithAuthor.vue" {10-16} + + + +``` + +### Embedded data + +When entities are stored in [nested structures](https://dataclient.io/rest/guides/relational-data#nesting), that structure will remain. + +```typescript title="api/Post" {12-16} +export class PaginatedPost extends Entity { + id = ''; + title = ''; + content = ''; + + static key = 'PaginatedPost'; +} + +export const getPosts = new RestEndpoint({ + path: '/post', + searchParams: { page: '' }, + schema: { + posts: new Collection([PaginatedPost]), + nextPage: '', + lastPage: '', + }, +}); +``` + +```html title="ArticleList.vue" + + + +``` From 826baf6b73a27593c737f6fc266035064fd82f6d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 03:28:11 +0000 Subject: [PATCH 14/15] docs(skills): Generate _vueArgs reference; copy Vue test utilities guide Symlinked references are rejected now, so data-client-vue lists _vueArgs in references.json, and data-client-vue-testing keeps the Vue test utilities guide as a hand-written reference (the generator only reads docs/). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WEpX2AVQj3Xmn2Q51om3J4 --- .../data-client-vue-testing/references.json | 3 +- .../references/vue-test-utilities.md | 307 ++++++++++++++++++ .../skills/data-client-vue/references.json | 1 + .../data-client-vue/references/_vueArgs.md | 6 +- 4 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 .agents/skills/data-client-vue-testing/references/vue-test-utilities.md mode change 120000 => 100644 .agents/skills/data-client-vue/references/_vueArgs.md diff --git a/.agents/skills/data-client-vue-testing/references.json b/.agents/skills/data-client-vue-testing/references.json index 9dab65c84d54..3e2b9c7b8c4d 100644 --- a/.agents/skills/data-client-vue-testing/references.json +++ b/.agents/skills/data-client-vue-testing/references.json @@ -3,7 +3,6 @@ "vue" ], "docs": { - "Fixtures.md": "docs/core/api/Fixtures.md", - "vue-test-utilities.md": "packages/vue/src/test/README.md" + "Fixtures.md": "docs/core/api/Fixtures.md" } } diff --git a/.agents/skills/data-client-vue-testing/references/vue-test-utilities.md b/.agents/skills/data-client-vue-testing/references/vue-test-utilities.md new file mode 100644 index 000000000000..b9a9cd50af35 --- /dev/null +++ b/.agents/skills/data-client-vue-testing/references/vue-test-utilities.md @@ -0,0 +1,307 @@ +# Vue Data Client Testing Utilities + +This package provides testing utilities for Vue applications using `@data-client/vue`, similar to `@data-client/test` for React. + +## Installation + +```bash +npm install @data-client/vue +``` + +## Usage + +### Basic Component Testing + +```typescript +import { mountDataClient } from '@data-client/vue/test'; +import { defineComponent, h } from 'vue'; +import { MyResource } from './resources'; + +const TestComponent = defineComponent({ + setup() { + return () => h('div', 'Hello World'); + }, +}); + +const { wrapper, controller, cleanup } = mountDataClient(TestComponent, { + initialFixtures: [ + { + endpoint: MyResource.get, + args: [{ id: 1 }], + response: { id: 1, name: 'Test' }, + }, + ], +}); + +// Your tests here +expect(wrapper.text()).toBe('Hello World'); + +// Clean up after test +cleanup(); +``` + +### Testing with Suspense (Automatic) + +Suspense is automatically integrated into `mountDataClient`. Your components will automatically suspend and show a fallback while data is loading: + +```typescript +import { mountDataClient } from '@data-client/vue/test'; +import { defineComponent, h } from 'vue'; +import { MyResource } from './resources'; + +const AsyncComponent = defineComponent({ + async setup() { + const data = await useSuspense(MyResource.get, { id: 1 }); + return () => h('div', data.value.name); + }, +}); + +const { wrapper, controller, cleanup } = mountDataClient(AsyncComponent); + +// Initially shows suspense fallback +expect(wrapper.find('[data-testid="suspense-fallback"]').exists()).toBe(true); + +// Wait for data to load +await flushUntil(wrapper, () => wrapper.find('div').exists()); + +// Now shows the actual content +expect(wrapper.text()).toBe('Test'); + +cleanup(); +``` + +### Composable Testing + +```typescript +import { renderDataCompose } from '@data-client/vue/test'; +import { reactive } from 'vue'; + +const useMyComposable = (props: { id: number }) => { + const data = useSuspense(MyResource.get, { id: props.id }); + return { data, isLoading: false }; +}; + +const props = reactive({ id: 1 }); +const { result, controller, cleanup, waitForNextUpdate } = renderDataCompose(useMyComposable, { + props, + initialFixtures: [ + { + endpoint: MyResource.get, + args: [{ id: 1 }], + response: { id: 1, name: 'Test' }, + }, + ], +}); + +// Initially suspended +expect(result.current).toBeUndefined(); + +// Wait for the composable to resolve +await waitForNextUpdate(); + +// Now should have the actual data (useSuspense returns a Promise that resolves to ComputedRef) +expect(result.current).toBeDefined(); +expect(result.current).toBeInstanceOf(Promise); + +// Access the actual data +const dataRef = await result.current; +expect(dataRef.value).toBeDefined(); + +// Update props reactively +props.id = 2; + +cleanup(); +``` + +### Testing with Reactive Props + +Components can receive reactive props that can be updated during tests: + +```typescript +import { mountDataClient } from '@data-client/vue/test'; +import { defineComponent, h, reactive } from 'vue'; +import { MyResource } from './resources'; + +const ArticleComponent = defineComponent({ + props: { + id: { + type: Number, + required: true, + }, + }, + async setup(props) { + // a getter so the fetch follows props.id + const article = await useSuspense(MyResource.get, () => ({ id: props.id })); + return () => h('div', article.value.title); + }, +}); + +// Create a reactive props ref +const props = reactive({ id: 1 }); + +const { wrapper, cleanup } = mountDataClient(ArticleComponent, { + props, + resolverFixtures: [ + { + endpoint: MyResource.get, + response: (request) => ({ + id: request.args[0].id, + title: `Article ${request.args[0].id}`, + }), + }, + ], +}); + +// Wait for initial render +await flushUntil(wrapper, () => wrapper.find('div').exists()); +expect(wrapper.text()).toBe('Article 1'); + +// Update props reactively - component will re-render with new data +props.id = 2; +await nextTick(); + +// Wait for new data to load +await flushUntil(wrapper, () => wrapper.text() === 'Article 2'); +expect(wrapper.text()).toBe('Article 2'); + +cleanup(); +``` + +### Using Resolver Fixtures + +```typescript +const { wrapper, controller, cleanup } = mountDataClient(TestComponent, { + resolverFixtures: [ + { + endpoint: MyResource.get, + response: (request) => { + return { + id: request.args[0].id, + name: `Dynamic ${request.args[0].id}`, + }; + }, + }, + ], +}); + +// Test dynamic responses +const result = await controller.fetch(MyResource.get, { id: 123 }); +expect(result.name).toBe('Dynamic 123'); +``` + +## API Reference + +### `mountDataClient(component, options)` + +Renders a Vue component with DataClient provider for testing. + +**Parameters:** +- `component`: Vue component to render +- `options`: Configuration options + +**Returns:** +- `wrapper`: Vue Test Utils wrapper +- `controller`: DataClient controller instance +- `app`: Vue app instance +- `cleanup`: Function to clean up resources +- `allSettled`: Function to wait for all pending promises + +### `renderDataCompose(composable, options)` + +Renders a Vue composable with DataClient provider for testing. + +**Parameters:** +- `composable`: Vue composable function +- `options`: Configuration options + +**Returns:** +- `result`: Object with `current` property containing the composable's return value (`undefined` when suspended, Promise when resolved) +- `wrapper`: Vue Test Utils wrapper +- `controller`: DataClient controller instance +- `cleanup`: Function to clean up resources +- `allSettled`: Function to wait for all pending promises +- `waitForNextUpdate`: Function to wait for the composable to resolve from suspended state + +### Options + +```typescript +interface RenderDataClientOptions

{ + props?: Reactive

; // Reactive props ref to pass to component + initialFixtures?: readonly Fixture[]; // Initial data fixtures + resolverFixtures?: readonly (Fixture | Interceptor)[]; // Dynamic response fixtures + getInitialInterceptorData?: () => any; // Initial data for interceptors + managers?: Manager[]; // Custom managers + initialState?: State; // Custom initial state + gcPolicy?: GCInterface; // Custom garbage collection policy + wrapper?: any; // Custom wrapper component +} +``` + +### Fixture Types + +```typescript +interface Fixture { + endpoint: FixtureEndpoint; + args: any[]; + response: any; + error?: boolean; +} + +interface Interceptor { + endpoint: FixtureEndpoint; + response: (request: { + body?: any; + headers?: Record; + url: string; + method: string; + args: any[]; + }) => any | Promise; + error?: boolean; +} +``` + +## Best Practices + +1. **Always call cleanup()** after your tests to prevent memory leaks +2. **Use fixtures** instead of mocking network requests when possible +3. **Test both success and error cases** using the `error` property in fixtures +4. **Use resolver fixtures** for dynamic responses based on request parameters +5. **Leverage the controller** for testing mutations and state updates +6. **Suspense is automatic** - no need to manually wrap components in Suspense +7. **Use reactive props** - Pass a `reactive` in the `props` option and set its members to change component props +8. **Vue Suspense behavior** - Vue's `useSuspense` returns a Promise that suspends when data is missing, then resolves to a ComputedRef +9. **Reactive props with async setup** - Async setup runs once per component instance, so pass prop-derived arguments as a getter (`() => ({ id: props.id })`); a plain object is read once and won't follow prop changes. + +## Migration from Manual Setup + +If you were previously setting up DataClient manually in tests: + +```typescript +// Before +const wrapper = mount(MyComponent, { + global: { + plugins: [ + [ + DataClientPlugin, + { + managers: [new NetworkManager()], + initialState: mockState, + }, + ], + ], + }, +}); + +// After +const { wrapper, controller, cleanup } = mountDataClient(MyComponent, { + initialFixtures: [ + { + endpoint: MyResource.get, + args: [{ id: 1 }], + response: { id: 1, name: 'Test' }, + }, + ], +}); +``` + +This approach is more declarative, easier to maintain, and provides better test isolation. diff --git a/.agents/skills/data-client-vue/references.json b/.agents/skills/data-client-vue/references.json index 9480208b976b..35c246e63c15 100644 --- a/.agents/skills/data-client-vue/references.json +++ b/.agents/skills/data-client-vue/references.json @@ -10,6 +10,7 @@ "_pagination.md": "docs/core/shared/_pagination.mdx", "_useLive.md": "docs/core/shared/_useLive.mdx", "_useLoading.md": "docs/core/shared/_useLoading.mdx", + "_vueArgs.md": "docs/core/shared/_vueArgs.mdx", "data-dependency.md": "docs/core/getting-started/data-dependency.md", "getDefaultManagers.md": "docs/core/api/getDefaultManagers.md", "installation.md": "docs/core/getting-started/installation.md", diff --git a/.agents/skills/data-client-vue/references/_vueArgs.md b/.agents/skills/data-client-vue/references/_vueArgs.md deleted file mode 120000 index 00a326155d94..000000000000 --- a/.agents/skills/data-client-vue/references/_vueArgs.md +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/core/shared/_vueArgs.mdx \ No newline at end of file diff --git a/.agents/skills/data-client-vue/references/_vueArgs.md b/.agents/skills/data-client-vue/references/_vueArgs.md new file mode 100644 index 000000000000..b3e3e6504d6b --- /dev/null +++ b/.agents/skills/data-client-vue/references/_vueArgs.md @@ -0,0 +1,5 @@ + + +Arguments can be plain values, [refs](https://vuejs.org/api/reactivity-core.html#ref) (including [computed](https://vuejs.org/api/reactivity-core.html#computed)), or getter +functions like `() => ({ id: props.id })`. A plain object like `{ id: props.id }` is read once and won't +follow prop or route changes, so use a getter or `computed` when an argument can change. From 0e3e0084315c47fd812aa8c996d457d6bfc28b21 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 03:39:15 +0000 Subject: [PATCH 15/15] docs(skills): Drop stale :::vue note from data-client-vue references Its references are generated for Vue only, so they have no framework sections. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WEpX2AVQj3Xmn2Q51om3J4 --- .agents/skills/data-client-vue/SKILL.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.agents/skills/data-client-vue/SKILL.md b/.agents/skills/data-client-vue/SKILL.md index 98663a149d03..1ddcf0360a45 100644 --- a/.agents/skills/data-client-vue/SKILL.md +++ b/.agents/skills/data-client-vue/SKILL.md @@ -166,8 +166,7 @@ This is useful for websockets, SSE, logging, etc. Pass them to `DataClientPlugin # References -For detailed API documentation, see the [references](references/) directory. They cover React and Vue; -read the `:::vue` sections. +For detailed API documentation, see the [references](references/) directory: - [useSuspense](references/useSuspense.md);[_pagination.md](references/_pagination.md) - Fetch with Suspense - [_vueArgs](references/_vueArgs.md) - Plain, ref, computed, and getter arguments