From vue
Explains Vue 3 Proxy-based reactivity with refs, reactive, shallowRef, customRef, computed, and watchers for managing complex state in applications.
npx claudepluginhub thebushidocollective/han --plugin vueThis skill is limited to using the following tools:
Master Vue's reactivity system to build reactive, performant applications
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Automates semantic versioning and release workflow for Claude Code plugins: bumps versions in package.json, marketplace.json, plugin.json; verifies builds; creates git tags, GitHub releases, changelogs.
Master Vue's reactivity system to build reactive, performant applications with optimal state management and computed properties.
Vue 3 uses JavaScript Proxies for reactivity:
import { ref, reactive, isRef, isReactive, isProxy } from 'vue';
// ref creates reactive wrapper
const count = ref(0);
console.log(isRef(count)); // true
console.log(isProxy(count)); // false (ref itself isn't proxy)
console.log(isProxy(count.value)); // false for primitives
// reactive creates proxy
const state = reactive({ count: 0 });
console.log(isReactive(state)); // true
console.log(isProxy(state)); // true
// Proxies track access and mutations
state.count++; // Triggers reactivity
count.value++; // Triggers reactivity
import { ref } from 'vue';
// Primitives
const count = ref(0);
const name = ref('John');
const isActive = ref(true);
// Access via .value
console.log(count.value); // 0
count.value++; // Update triggers reactivity
// Objects (wrapped in proxy)
const user = ref({
name: 'John',
age: 30
});
// Nested properties are reactive
user.value.age++; // Triggers reactivity
// Can replace entire object
user.value = { name: 'Jane', age: 25 }; // Works!
import { shallowRef, triggerRef } from 'vue';
// Only .value is reactive, not nested properties
const state = shallowRef({
count: 0,
nested: { value: 0 }
});
// This triggers reactivity
state.value = { count: 1, nested: { value: 1 } };
// This does NOT trigger reactivity
state.value.count++; // No update!
// Manually trigger
state.value.count++;
triggerRef(state); // Force update
import { customRef } from 'vue';
// Debounced ref
function useDebouncedRef<T>(value: T, delay = 200) {
let timeout: ReturnType<typeof setTimeout>;
return customRef((track, trigger) => ({
get() {
track(); // Tell Vue to track this
return value;
},
set(newValue: T) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger(); // Tell Vue to re-render
}, delay);
}
}));
}
// Usage
const searchQuery = useDebouncedRef('', 300);
// Updates are debounced
searchQuery.value = 'a'; // Doesn't trigger immediately
searchQuery.value = 'ab'; // Still waiting
searchQuery.value = 'abc'; // Triggers after 300ms
import { reactive } from 'vue';
// Create deep reactive object
const state = reactive({
user: {
name: 'John',
profile: {
email: 'john@example.com',
settings: {
theme: 'dark'
}
}
},
posts: []
});
// All nested properties are reactive
state.user.profile.settings.theme = 'light'; // Triggers reactivity
state.posts.push({ id: 1, title: 'Post' }); // Triggers reactivity
import { shallowReactive } from 'vue';
// Only root-level properties are reactive
const state = shallowReactive({
count: 0,
nested: { value: 0 }
});
// This triggers reactivity
state.count++; // Works
// This does NOT trigger reactivity
state.nested.value++; // No update!
// But replacing works
state.nested = { value: 1 }; // Triggers reactivity
import { reactive } from 'vue';
const list = reactive<number[]>([]);
// Mutating methods trigger reactivity
list.push(1); // Reactive
list.pop(); // Reactive
list.splice(0, 1); // Reactive
list.sort(); // Reactive
list.reverse(); // Reactive
// Replacement triggers reactivity
const newList = reactive([1, 2, 3]);
import { reactive } from 'vue';
// Map
const map = reactive(new Map<string, number>());
map.set('count', 0); // Reactive
map.delete('count'); // Reactive
// Set
const set = reactive(new Set<number>());
set.add(1); // Reactive
set.delete(1); // Reactive
// WeakMap and WeakSet
const weakMap = reactive(new WeakMap());
const weakSet = reactive(new WeakSet());
import { reactive, readonly, isReadonly } from 'vue';
const state = reactive({ count: 0 });
const readonlyState = readonly(state);
console.log(isReadonly(readonlyState)); // true
// Cannot mutate
readonlyState.count++; // Warning in dev mode
// Original is still mutable
state.count++; // Works, updates readonly view too
// Deep readonly
const deepState = reactive({
nested: { value: 0 }
});
const deepReadonly = readonly(deepState);
deepReadonly.nested.value++; // Warning! Deep readonly
import { reactive, toRefs } from 'vue';
const state = reactive({
count: 0,
name: 'John'
});
// Destructuring loses reactivity
const { count, name } = state; // NOT reactive!
// Use toRefs to preserve reactivity
const { count: countRef, name: nameRef } = toRefs(state);
// Now reactive
countRef.value++; // Updates state.count
console.log(state.count); // 1
import { reactive, toRef } from 'vue';
const state = reactive({
count: 0
});
// Create ref to specific property
const countRef = toRef(state, 'count');
countRef.value++; // Updates state.count
console.log(state.count); // 1
// Non-existent properties
const missingRef = toRef(state, 'missing');
missingRef.value = 'now exists'; // Adds to state!
import { ref, unref, isRef } from 'vue';
const count = ref(0);
const plain = 0;
// unref: unwrap if ref, return value otherwise
console.log(unref(count)); // 0
console.log(unref(plain)); // 0
// Useful for handling ref or value
function double(value: number | Ref<number>): number {
return unref(value) * 2;
}
double(count); // 0
double(5); // 10
// isRef: check if value is ref
if (isRef(count)) {
console.log(count.value);
} else {
console.log(count);
}
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(() => count.value * 2);
console.log(doubled.value); // 0
count.value = 5;
console.log(doubled.value); // 10
// Computed is cached
const expensive = computed(() => {
console.log('Computing...');
return count.value * 2;
});
console.log(expensive.value); // Computing... 0
console.log(expensive.value); // 0 (cached, no log)
count.value = 1;
console.log(expensive.value); // Computing... 2
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
[firstName.value, lastName.value] = value.split(' ');
}
});
console.log(fullName.value); // John Doe
fullName.value = 'Jane Smith';
console.log(firstName.value); // Jane
console.log(lastName.value); // Smith
import { ref, computed } from 'vue';
const count = ref(0);
const doubled = computed(
() => count.value * 2,
{
onTrack(e) {
console.log('Tracked:', e);
},
onTrigger(e) {
console.log('Triggered:', e);
}
}
);
import { ref, watch } from 'vue';
const count = ref(0);
// Basic watch
watch(count, (newValue, oldValue) => {
console.log(`Count: ${oldValue} -> ${newValue}`);
});
// With options
watch(
count,
(newValue, oldValue) => {
console.log('Count changed');
},
{
immediate: true, // Run immediately
flush: 'post', // Timing: 'pre' | 'post' | 'sync'
onTrack(e) { console.log('Tracked:', e); },
onTrigger(e) { console.log('Triggered:', e); }
}
);
import { ref, watch } from 'vue';
const x = ref(0);
const y = ref(0);
// Watch array of sources
watch(
[x, y],
([newX, newY], [oldX, oldY]) => {
console.log(`x: ${oldX} -> ${newX}`);
console.log(`y: ${oldY} -> ${newY}`);
}
);
// Trigger when any changes
x.value++; // Logs
y.value++; // Logs
import { reactive, watch } from 'vue';
const state = reactive({
count: 0,
user: { name: 'John' }
});
// Watch getter
watch(
() => state.count,
(newValue, oldValue) => {
console.log('Count changed');
}
);
// Deep watch entire object
watch(
state,
(newValue, oldValue) => {
console.log('State changed');
},
{ deep: true }
);
// Watch specific nested property
watch(
() => state.user.name,
(newValue, oldValue) => {
console.log('Name changed');
}
);
import { ref, watch } from 'vue';
const count = ref(0);
const stop = watch(count, (value) => {
console.log(`Count: ${value}`);
// Stop watching when count reaches 5
if (value >= 5) {
stop();
}
});
// Or stop externally
stop();
import { ref, watchEffect } from 'vue';
const count = ref(0);
const name = ref('John');
// Automatically tracks dependencies
watchEffect(() => {
console.log(`${name.value}: ${count.value}`);
});
// Logs: John: 0
count.value++; // Logs: John: 1
name.value = 'Jane'; // Logs: Jane: 1
// Cleanup
const stop = watchEffect((onCleanup) => {
const timer = setTimeout(() => {
console.log(count.value);
}, 1000);
// Register cleanup
onCleanup(() => {
clearTimeout(timer);
});
});
// Stop watching
stop();
import { ref, watchEffect, watchPostEffect, watchSyncEffect } from 'vue';
const count = ref(0);
// Default: 'pre' - before component update
watchEffect(() => {
console.log('Pre:', count.value);
}, { flush: 'pre' });
// 'post' - after component update (access updated DOM)
watchPostEffect(() => {
console.log('Post:', count.value);
// Can access updated DOM
});
// 'sync' - synchronous (use sparingly!)
watchSyncEffect(() => {
console.log('Sync:', count.value);
});
import { effectScope, ref, watch } from 'vue';
const scope = effectScope();
scope.run(() => {
const count = ref(0);
watch(count, () => {
console.log('Count changed');
});
watchEffect(() => {
console.log('Effect');
});
});
// Stop all effects in scope
scope.stop();
// Nested scopes
const parent = effectScope();
parent.run(() => {
const child = effectScope();
child.run(() => {
// Child effects
});
// Stop child only
child.stop();
});
// Stop parent (and all children)
parent.stop();
import { ref, triggerRef } from 'vue';
const count = ref(0);
// Manually trigger updates
count.value = 1;
triggerRef(count); // Force update even if value didn't change
import { reactive, ref } from 'vue';
const count = ref(0);
const state = reactive({
// Refs are auto-unwrapped in reactive
count
});
// No .value needed
console.log(state.count); // 0 (not state.count.value)
state.count++; // Works
// But in arrays, not unwrapped
const list = reactive([ref(0)]);
console.log(list[0].value); // Must use .value
Use vue-reactivity-system when building modern, production-ready applications that require:
ref for primitives - Always wrap primitives in refreactive for objects - Deep reactivity for complex statecomputed for derived state - Cached and reactivewatch for side effects - API calls, localStorage, etc.watchEffect for simple effects - Auto-tracks dependenciestoRefs to preserve reactivityreadonly to prevent mutations - Protect shared stateonCleanupshallowRef/shallowReactive for large data - Better performancetoRefs.value on refs - Common source of bugsref insteadimport { reactive, readonly, computed } from 'vue';
interface State {
count: number;
items: string[];
}
function createStore() {
const state = reactive<State>({
count: 0,
items: []
});
// Computed
const doubled = computed(() => state.count * 2);
// Actions
function increment() {
state.count++;
}
function addItem(item: string) {
state.items.push(item);
}
// Expose readonly state
return {
state: readonly(state),
doubled,
increment,
addItem
};
}
const store = createStore();
import { reactive, computed, watch } from 'vue';
interface FormData {
email: string;
password: string;
}
interface FormErrors {
email?: string;
password?: string;
}
function useForm() {
const data = reactive<FormData>({
email: '',
password: ''
});
const errors = reactive<FormErrors>({});
const isValid = computed(() =>
!errors.email && !errors.password &&
data.email && data.password
);
// Validate on change
watch(
() => data.email,
(email) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
errors.email = 'Invalid email';
} else {
delete errors.email;
}
}
);
watch(
() => data.password,
(password) => {
if (password.length < 8) {
errors.password = 'Must be 8+ characters';
} else {
delete errors.password;
}
}
);
return {
data,
errors,
isValid
};
}
import { ref, watchEffect } from 'vue';
interface User {
id: number;
name: string;
}
function useAsyncData<T>(
fetcher: () => Promise<T>
) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
async function execute() {
loading.value = true;
error.value = null;
try {
data.value = await fetcher();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
watchEffect((onCleanup) => {
let cancelled = false;
execute().then(() => {
if (cancelled) {
data.value = null;
}
});
onCleanup(() => {
cancelled = true;
});
});
return { data, error, loading, refetch: execute };
}
import { reactive } from 'vue';
const state = reactive<{ count?: number }>({});
// Adding new property is reactive
state.count = 1; // Reactive
// But TypeScript won't know about it unless typed
// Solution: Define all properties upfront or use proper types
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
</script>
<template>
<!-- Auto-unwrapped in templates -->
<p>{{ count }}</p> <!-- Not count.value -->
<!-- But not in JavaScript expressions -->
<p>{{ count + 1 }}</p> <!-- Won't work! -->
<p>{{ count.value + 1 }}</p> <!-- Correct -->
</template>
import { reactive } from 'vue';
// Primitive values in reactive are still reactive
const state = reactive({
count: 0 // Reactive
});
// But extracting loses reactivity
let count = state.count; // Not reactive
count++; // Doesn't update state