web-utilities-vueuse-42ae5b — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited web-utilities-vueuse-42ae5b (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Quick Guide: VueUse wraps browser APIs, sensors, network transports and small state utilities as composables that return refs and clean themselves up on scope disposal. Import each function by name from @vueuse/core — the library is large and entirely tree-shakeable. Two things decide whether a call works: it must run synchronously during setup, because teardown binds to the effect scope that is current at call time; and browser-API composables behave differently on a server, some returning a safe default and some needing client-only rendering.Detailed Resources:
useFetch, createFetch, useWebSocket, useEventSourcecreateGlobalState, useRefHistory, syncRef, persistenceuseVModel, useVirtualList, onClickOutside, onKeyStroke, animationServer rendering is the branch, because it changes which composables are usable at all.
useMouse, useScroll, useIntersectionObserver, useWebSocket, useClipboard) need a DOM and belong inside a client-only boundary. The table in reference.md says which is which.isSupported ref and render a fallback, rather than assuming the call worked.<critical_requirements>
Call composables synchronously during setup. Teardown registers against the effect scope that is current when the composable runs, so a call inside an await, a setTimeout or an event handler has no scope to attach to and its listeners are never removed.
Import each composable by name from `@vueuse/core`. A namespace import pulls the whole library past the bundler's tree-shaker, and the library is several hundred functions.
Read `isSupported` where the composable wraps an optional browser API — clipboard, share, geolocation, battery, wake lock. It exists because the API can be absent or permission-gated, and the fallback path is what the user sees when it is.
Decide the server behaviour before the composable ships. Each one either returns a declared default on the server or needs a client-only boundary; guessing produces a hydration mismatch, which reports as a rendering bug far from its cause.
</critical_requirements>
Auto-detection: VueUse, vueuse, @vueuse/core, @vueuse/integrations, useLocalStorage, useSessionStorage, useClipboard, useFetch, createFetch, useMouse, useMouseInElement, useScroll, useIntersectionObserver, useResizeObserver, useElementVisibility, useElementSize, useMediaQuery, usePreferredDark, useDark, useToggle, useCounter, useCycleList, useWebSocket, useEventSource, useEventListener, useTransition, useRafFn, createGlobalState, useRefHistory, useManualRefHistory, syncRef, useVModel, useVirtualList, onClickOutside, onKeyStroke, useDebounceFn, watchDebounced
Applies to:
Handled elsewhere:
ref, computed, watch and effect scopes are the framework's, and these composables are built on top of them<philosophy>
Each composable owns one concern and its own teardown. That is the whole contract: it returns refs, it registers whatever listener or observer it needs, and it disposes of them when the surrounding effect scope ends. Nothing is registered globally and nothing needs a plugin.
The consequence is that the _call site_ owns the lifetime. Two components calling useMouse() get two independent listeners, both cleaned up separately — which is why sharing one instance is an explicit act (createGlobalState, or a composable of your own) rather than something the library does for you.
</philosophy>
<patterns>
import { useLocalStorage } from "@vueuse/core";
const settings = useLocalStorage("app-settings", DEFAULTS, {
mergeDefaults: true, // stored data gains fields added to DEFAULTS since it was written
});
settings.value.theme = "dark"; // persists, and other tabs see it
settings.value = null; // removes the key entirely — it does not store nullSerialisation, cross-tab sync via the storage event, and a server-side default all come with it. Full code: examples/core.md
import { useEventListener } from "@vueuse/core";
import { useTemplateRef } from "vue";
const dropZone = useTemplateRef("drop-zone");
useEventListener(window, "resize", onResize);
useEventListener(dropZone, "drop", onDrop); // waits for the ref to be populatedPassing a ref rather than an element is the point: the composable attaches once the element exists and re-attaches if it changes, so there is no mount-order problem to solve by hand.
Full code: examples/core.md
import {
useMediaQuery,
usePreferredDark,
useDark,
useToggle,
} from "@vueuse/core";
const isMobile = useMediaQuery("(max-width: 768px)");
const prefersDark = usePreferredDark(); // reads the OS setting
const isDark = useDark(); // reads it, persists an override, toggles a class
const toggleDark = useToggle(isDark);usePreferredDark observes; useDark also writes — it persists the choice and toggles a class on the root element, so the class name it uses has to match what the stylesheet expects.
Full code: examples/core.md
import { useScroll, useIntersectionObserver } from "@vueuse/core";
const { y, directions, arrivedState } = useScroll(container);
useIntersectionObserver(
sentinel,
([entry]) => {
if (entry?.isIntersecting) loadMore();
},
{ rootMargin: "200px" },
); // fire before it reaches the viewportarrivedState and directions are the parts worth knowing — they replace the offset arithmetic that scroll handlers usually get wrong at the edges.
Full code: examples/sensors.md
import { useFetch } from "@vueuse/core";
const url = computed(() => `/api/users/${userId.value}`);
// Re-runs whenever `url` changes, and aborts the previous request
const { data, isFetching, error, abort } = useFetch(url, {
refetch: true,
}).json<User>();
// Manual: no request until execute() is called
const { execute } = useFetch("/api/users", { immediate: false })
.post(body)
.json();The chained .json<T>() is what types data; without it you hold a Ref<string | null>.
Full code: examples/network.md
import { createGlobalState } from "@vueuse/core";
export const useGlobalCounter = createGlobalState(() => {
const count = shallowRef(0);
const double = computed(() => count.value * 2);
const increment = () => count.value++;
return { count, double, increment };
});The factory runs once and every caller gets the same refs. Returning actions rather than the bare writable refs is what keeps the mutation points countable.
Full code: examples/state.md
import { useVModel } from "@vueuse/core";
const value = useVModel(props, "modelValue", emit);
const count = useVModel(props, "count", emit); // named v-model
value.value = next; // emits update:modelValueOne line where the get/set computed is five, and the event name is derived rather than typed out — which is where the typo used to live.
Full code: examples/component.md
</patterns>
<red_flags>
Breaks at runtime:
await, setTimeout or an event handler binds to no effect scope: its listeners and observers are never disposed of, and the component leaks them on every mount.window or document and throws. Move it behind a client-only boundary.isSupported fails on the browsers and contexts where the API is missing or permission-gated — clipboard write outside a user gesture is the usual first case.@vueuse/core defeats tree-shaking and ships the whole library.Surprising behaviour:
useLocalStorage(...).value = null deletes the key rather than storing null. Store an explicit empty value where the key must survive.createGlobalState is a singleton for the module's lifetime — it survives every component unmounting and is cleared only by a reload, which makes it a poor fit for per-user state that must reset on sign-out.useIntersectionObserver invokes its callback once on registration with the element's current state, so "became visible" needs comparing against the previous value rather than trusting the first call.useFetch fires on mount by default; refetch: true adds re-firing on URL change without removing the initial one. immediate: false is what makes it fully manual.useWebSocket reconnects by default, so a server rejecting the connection produces a retry loop until autoReconnect: false or a bounded retries says otherwise.useRefHistory snapshots every change, which on a text input is one entry per keystroke. Give it a capacity, or use useManualRefHistory and commit at save points.shallowRef avoids deep-reactivity cost on large objects, but a composable that persists or watches nested changes — useLocalStorage among them — needs the deep tracking to see them at all. Choose per composable rather than as a blanket rule.</red_flags>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.