Read, change and render async and shared state in Vue with @data-client/vue: any data from an API or other async source, and global state shared across components (it has to persist somewhere, which makes it async). Use it for loading and error states, mutations and forms, filtered or derived views, and debugging data that looks wrong or stale on screen. Use it in place of fetch in onMounted or watch, or app-wide refs and provide/inject; its main composables are useSuspense, useQuery and useController.
97%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
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.
// 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; an endpoint without params takes {}
const user = await useSuspense(UserResource.get, () => (userId.value ? { id: userId.value } : null));For API definitions (like TodoResource), apply the skill "data-client-rest".
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.{{ todo.title }}); in script read .value (todo.value.title).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().
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).
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 refsconst query = ref(''); // bound with v-model
const [debouncedQuery, isPending] = useDebounce(query, 200);
// pass debouncedQuery to the component that fetches, inside <Suspense>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).
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 update | Call |
|---|---|
| You already have the new value | ctrl.set() / ctrl.setResponse() (no network) |
| One endpoint+args | ctrl.fetch(endpoint, args) |
| All cached args of an endpoint | ctrl.expireAll(endpoint): fetching composables refetch; useCache/useQuery don't |
| Current data must not show while refetching | ctrl.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.
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);When the running app shows the wrong data, inspect the page's store before reading code for the cause. In dev mode,
__DC__.controller is the page's Controller and __DC__.actions its recent actions, reachable
from any browser tool that runs JavaScript in the page. devtools-debugging maps
symptoms to what to check and links the setup for your browser tool.
Custom Managers allow for global side effect handling.
This is useful for websockets, SSE, logging, etc. Pass a module-level function that creates them, so each store gets its own:
const managers = () => [new MyManager(), ...getDefaultManagers()]; with
app.use(DataClientPlugin, { managers })
(getDefaultManagers). Always use the skill "data-client-manager" when writing managers.
Query schema (with the data model, e.g. src/resources/) so they stay reusable and evolve independently of viewsref()/reactive() state; render the returned refs directly so updates flow throughundefined 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 loadsFor detailed API documentation, see the references directory:
$dataClientDataClientPluginDataClientPlugin managersALWAYS 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.
reactive/data-client
e1ea9ce
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.