vue-reactivity-system

Vue Reactivity System

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "vue-reactivity-system" with this command: npx skills add thebushidocollective/han/thebushidocollective-han-vue-reactivity-system

Vue Reactivity System

Master Vue's reactivity system to build reactive, performant applications with optimal state management and computed properties.

Reactivity Fundamentals (Proxy-based)

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

Ref - Reactive Primitives and Objects

Basic Ref Usage

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!

Shallow Ref

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

Custom Ref

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

Reactive - Deep Reactive Objects

Basic Reactive Usage

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

Shallow Reactive

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

Reactive Arrays

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]);

Reactive Collections

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());

Readonly - Prevent Mutations

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

ToRef and ToRefs - Preserve Reactivity

ToRefs - Convert Reactive to Refs

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

ToRef - Create Ref from Property

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!

Unref and IsRef - Ref Utilities

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); }

Computed - Derived State

Basic Computed

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

Writable Computed

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

Computed Debugging

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); } } );

Watch - React to Changes

Watch Single Source

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); } } );

Watch Multiple Sources

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

Watch Reactive Object

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'); } );

Stop Watching

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();

WatchEffect - Automatic Dependency Tracking

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();

WatchEffect Timing

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); });

Effect Scope - Group Effects

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();

Reactivity Utilities

Trigger and Scheduler

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

Reactive Unwrapping

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

When to Use This Skill

Use vue-reactivity-system when building modern, production-ready applications that require:

  • Complex state management patterns

  • Fine-grained reactivity control

  • Performance optimization through computed properties

  • Advanced watching and effect patterns

  • Understanding of Vue's reactive internals

  • Debugging reactivity issues

  • Building reactive composables

  • Large-scale applications with complex data flows

Reactivity Best Practices

  • Use ref for primitives - Always wrap primitives in ref

  • Use reactive for objects - Deep reactivity for complex state

  • Use computed for derived state - Cached and reactive

  • Use watch for side effects - API calls, localStorage, etc.

  • Use watchEffect for simple effects - Auto-tracks dependencies

  • Don't destructure reactive - Use toRefs to preserve reactivity

  • Use readonly to prevent mutations - Protect shared state

  • Cleanup effects properly - Return cleanup function or use onCleanup

  • Avoid deep watching everything - Performance impact

  • Use shallowRef /shallowReactive for large data - Better performance

Common Reactivity Pitfalls

  • Destructuring reactive objects - Loses reactivity without toRefs

  • Forgetting .value on refs - Common source of bugs

  • Replacing reactive object - Breaks reactivity, use ref instead

  • Deep watching performance - Can be slow with large objects

  • Not cleaning up watchers - Memory leaks

  • Accessing refs before initialization - Can be undefined

  • Mutating props - Props are readonly

  • Unnecessary computed - Use regular refs if not derived

  • Synchronous effects - Usually should be async

  • Not understanding proxy limitations - Some operations don't track

Advanced Patterns

Reactive State Pattern

import { 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();

Reactive Form State

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 }; }

Async Reactive State

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 }; }

Reactivity Caveats and Limitations

Property Addition/Deletion

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

Ref Unwrapping in Templates

<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>

Non-Reactive Values

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

Resources

  • Vue 3 Reactivity Documentation

  • Reactivity in Depth

  • Reactivity API Reference

  • Reactivity Transform (Experimental)

  • Composition API FAQ

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

android-jetpack-compose

No summary provided by upstream source.

Repository SourceNeeds Review
General

fastapi-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

storybook-story-writing

No summary provided by upstream source.

Repository SourceNeeds Review
General

atomic-design-fundamentals

No summary provided by upstream source.

Repository SourceNeeds Review