CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

data-client-vue

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.

77

Quality

97%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Setup

Install DataClientPlugin once with app.use(DataClientPlugin) (installation; apply the skill "data-client-setup" to set it up). Composables only work inside <script setup> of components below it. Awaiting composables (await useSuspense()) requires <script setup>: in a hand-written async setup(), composables called after the first await lose the component instance.

Rendering

// GET https://jsonplaceholder.typicode.com/todos/5
const todo = await useSuspense(TodoResource.get, { id: 5 });
// GET https://jsonplaceholder.typicode.com/todos
const todoList = await useSuspense(TodoResource.getList);
// GET https://jsonplaceholder.typicode.com/todos?userId=1
const todoListByUser = await useSuspense(TodoResource.getList, { userId: 1 });
// subscriptions with polling, websockets or SSE
const todo = await useLive(TodoResource.get, { id: 5 });
// without fetch (not awaited)
const todo = useCache(TodoResource.get, { id: 5 });
const todo = useQuery(Todo, { id: 5 });
// fetch without Suspense - returns { data, loading, error } refs
const { data, loading, error } = useDLE(TodoResource.get, { id: 5 });
// subscribe without Suspense (use with useSuspense or useDLE)
useSubscription(TodoResource.get, { id: 5 });
// parallel fetches: start both, then await
useFetch(PostResource.get, { id });
useFetch(CommentResource.getList, { postId: id });
const post = await useSuspense(PostResource.get, { id });
const comments = await useSuspense(CommentResource.getList, { postId: id });
// conditional: null skips fetching and binding
const user = await useSuspense(UserResource.get, () => (userId.value ? { id: userId.value } : null));

For API definitions (like TodoResource), apply the skill "data-client-rest".

Return values are refs

  • useSuspense() and useLive() return a Promise of a readonly ComputedRef: await them at the top level of <script setup>. useCache(), useQuery(), and each of useDLE()'s data/loading/error are ComputedRefs.
  • Templates unwrap them ({{ todo.title }}); in script read .value (todo.value.title).

Reactive arguments

A plain object like { id: props.id } is read once and won't follow prop or route changes. When an argument can change, pass a getter (or a ref/computed) (arguments):

const props = defineProps<{ id: number }>();
// refetches when props.id changes
const todo = await useSuspense(TodoResource.get, () => ({ id: props.id }));

When arguments change, useSuspense() and useLive() keep the previous data until the new fetch resolves (no v-if guard needed); a failed fetch for the new arguments reaches onErrorCaptured().

Mutations

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. The Controller is also available as $dataClient in any template (and this.$dataClient in the Options API).

Helpful composables

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
const query = ref(''); // bound with v-model
const [debouncedQuery, isPending] = useDebounce(query, 200);
// pass debouncedQuery to the component that fetches, inside <Suspense>

Loading and error boundaries

There is no AsyncBoundary component in Vue. A component that awaits useSuspense() or useLive() must render inside Vue's <Suspense>; its #fallback slot is the loading state. Catch fetch errors in an ancestor with onErrorCaptured(). Reuse the codebase's existing boundary component if it has one; otherwise follow the boundary example (onErrorCaptured stores the error and returns false; the template shows it or renders <Suspense>). Place boundaries around route views or sections, not around each data-bound component (boundaries).

Type-safe imperative actions

Controller 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().

To refresh or updateCall
You already have the new valuectrl.set() / ctrl.setResponse() (no network)
One endpoint+argsctrl.fetch(endpoint, args)
All cached args of an endpointctrl.expireAll(endpoint): fetching composables refetch; useCache/useQuery don't
Current data must not show while refetchingctrl.invalidate() / ctrl.invalidateAll(), rendering useDLE's loading; mounted useSuspense keeps its last data

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

const queryRemainingTodos = new Query(
  TodoResource.getList.schema,
  entries => entries.filter(todo => !todo.completed).length,
);

const allRemainingTodos = useQuery(queryRemainingTodos);
const firstUserRemainingTodos = useQuery(queryRemainingTodos, { userId: 1 });
const groupTodoByUser = new Query(
  TodoResource.getList.schema,
  todos => Object.groupBy(todos, todo => todo.userId),
);
const todosByUser = useQuery(groupTodoByUser);

Browser Debugging (Chrome DevTools MCP)

When debugging a running app with a browser MCP (user-chrome-devtools), such as data that never appears, reverts, or refetches too often, read devtools-debugging before probing: its "Where to Start" table maps symptoms to the state and actions to check. Dev mode only: start with its debug shim, which every other snippet there depends on.

Managers

Custom Managers allow for global side effect handling. This is useful for websockets, SSE, logging, etc. Pass them to DataClientPlugin with getDefaultManagers. Always use the skill "data-client-manager" when writing managers.

Best Practices & Notes

  • useDebounce(query, timeout) when rendering async data based on user field inputs
  • [handleSubmit, loading, error] = useLoading() 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. Pass ids/args, not fetched data a component only hands down (a list passing each item to its row is fine); bindings with the same endpoint and args share one fetch and store entry
  • 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
  • undefined from useCache, or useQuery on an Entity, means "not loaded yet", not "doesn't exist"; a deep link or refresh starts empty. Read required data with useSuspense/useDLE, which fetch only when the cache is missing, stale or invalidated; use useCache/useQuery for data other code loads
  • For tests, apply the skill "data-client-vue-testing"

References

For detailed API documentation, see the references directory:

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.

Repository

reactive/data-client

Path

.agents/skills/data-client-vue/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.