Use when Vue 3 Composition API with reactive refs, computed, and composables. Use when building modern Vue 3 applications.
Generates Vue 3 Composition API code with reactive refs, computed properties, and composables for scalable applications.
npx claudepluginhub thebushidocollective/hanThis skill is limited to using the following tools:
Master the Vue 3 Composition API for building scalable, maintainable Vue applications with better code organization and reusability.
The setup() function is the entry point for using the Composition API:
import { ref, computed, onMounted } from 'vue';
export default {
props: ['initialCount'],
setup(props, context) {
// props is reactive
console.log(props.initialCount);
// context provides attrs, slots, emit, expose
const { attrs, slots, emit, expose } = context;
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
emit('update', count.value);
}
onMounted(() => {
console.log('Component mounted');
});
// Expose public methods
expose({ increment });
// Return values to template
return {
count,
doubled,
increment
};
}
};
Modern Vue 3 uses <script setup> for cleaner syntax:
<script setup lang="ts">
import { ref, computed } from 'vue';
// Top-level bindings automatically exposed to template
const count = ref(0);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
// Props and emits use compiler macros
interface Props {
initialCount?: number;
}
const props = withDefaults(defineProps<Props>(), {
initialCount: 0
});
const emit = defineEmits<{
update: [value: number];
}>();
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Doubled: {{ doubled }}</p>
<button @click="increment">Increment</button>
</div>
</template>
import { ref } from 'vue';
// Primitives
const count = ref(0);
const name = ref('John');
const isActive = ref(true);
// Single object that needs replacement
const user = ref({ name: 'John', age: 30 });
user.value = { name: 'Jane', age: 25 }; // Works
// Arrays that need replacement
const items = ref([1, 2, 3]);
items.value = [4, 5, 6]; // Works
import { reactive, toRefs } from 'vue';
// Complex nested objects
const state = reactive({
user: { name: 'John', age: 30 },
settings: { theme: 'dark', notifications: true },
posts: []
});
// Group related state
const formState = reactive({
name: '',
email: '',
password: '',
errors: {}
});
// Convert to refs for destructuring
const { name, email } = toRefs(formState);
// DON'T: Replacing entire reactive object loses reactivity
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); // Breaks reactivity!
// DO: Use ref instead
const state = ref({ count: 0 });
state.value = { count: 1 }; // Works
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
const names = value.split(' ');
firstName.value = names[0] || '';
lastName.value = names[1] || '';
}
});
// Can now set
fullName.value = 'Jane Smith';
interface Product {
id: number;
name: string;
price: number;
quantity: number;
}
const cart = ref<Product[]>([]);
const cartSummary = computed(() => {
const total = cart.value.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const itemCount = cart.value.reduce((sum, item) =>
sum + item.quantity, 0
);
const tax = total * 0.08;
const grandTotal = total + tax;
return {
total,
itemCount,
tax,
grandTotal
};
});
import { ref, watch } from 'vue';
const count = ref(0);
const name = ref('');
// Watch single source
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
// Watch multiple sources
watch(
[count, name],
([newCount, newName], [oldCount, oldName]) => {
console.log('Multiple values changed');
}
);
// Watch reactive object property
const user = reactive({ name: 'John', age: 30 });
watch(
() => user.name,
(newName) => {
console.log(`Name changed to ${newName}`);
}
);
// Deep watch
watch(
user,
(newUser) => {
console.log('User changed:', newUser);
},
{ deep: true }
);
import { ref, watchEffect } from 'vue';
const count = ref(0);
const multiplier = ref(2);
// Automatically tracks dependencies
watchEffect(() => {
console.log(`Result: ${count.value * multiplier.value}`);
});
// Runs immediately and whenever dependencies change
const data = ref(null);
watch(
source,
(newValue, oldValue) => {
// Callback logic
},
{
immediate: true, // Run immediately
deep: true, // Deep watch objects
flush: 'post', // Timing: 'pre' | 'post' | 'sync'
onTrack(e) { // Debug
console.log('tracked', e);
},
onTrigger(e) { // Debug
console.log('triggered', e);
}
}
);
// Stop watching
const stop = watch(source, callback);
stop(); // Cleanup
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated,
onDeactivated
} from 'vue';
export default {
setup() {
onBeforeMount(() => {
console.log('Before mount');
});
onMounted(() => {
console.log('Mounted');
// DOM is available
// Setup event listeners, fetch data
});
onBeforeUpdate(() => {
console.log('Before update');
});
onUpdated(() => {
console.log('Updated');
// DOM has been updated
});
onBeforeUnmount(() => {
console.log('Before unmount');
// Cleanup before unmount
});
onUnmounted(() => {
console.log('Unmounted');
// Final cleanup
});
onErrorCaptured((err, instance, info) => {
console.error('Error captured:', err, info);
return false; // Stop propagation
});
// For components wrapped in <KeepAlive>
onActivated(() => {
console.log('Component activated');
});
onDeactivated(() => {
console.log('Component deactivated');
});
}
};
// composables/useCounter.ts
import { ref, computed } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const doubled = computed(() => count.value * 2);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
function reset() {
count.value = initialValue;
}
return {
count: readonly(count),
doubled,
increment,
decrement,
reset
};
}
// Usage
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';
const { count, doubled, increment, decrement } = useCounter(10);
</script>
// composables/useFetch.ts
import { ref, unref, watchEffect } from 'vue';
import type { Ref } from 'vue';
export function useFetch<T>(url: Ref<string> | string) {
const data = ref<T | null>(null);
const error = ref<Error | null>(null);
const loading = ref(false);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch(unref(url));
if (!response.ok) throw new Error('Fetch failed');
data.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
watchEffect(() => {
fetchData();
});
return {
data: readonly(data),
error: readonly(error),
loading: readonly(loading),
refetch: fetchData
};
}
// Usage
<script setup lang="ts">
import { ref } from 'vue';
import { useFetch } from '@/composables/useFetch';
const userId = ref('1');
const url = computed(() => `/api/users/${userId.value}`);
const { data, error, loading, refetch } = useFetch(url);
</script>
// composables/useEventListener.ts
import { onMounted, onUnmounted } from 'vue';
export function useEventListener(
target: EventTarget,
event: string,
handler: (e: Event) => void
) {
onMounted(() => {
target.addEventListener(event, handler);
});
onUnmounted(() => {
target.removeEventListener(event, handler);
});
}
// Usage
<script setup lang="ts">
import { useEventListener } from '@/composables/useEventListener';
useEventListener(window, 'resize', () => {
console.log('Window resized');
});
</script>
<script setup lang="ts">
interface Props {
title: string;
count?: number;
items: string[];
user: {
name: string;
email: string;
};
}
const props = withDefaults(defineProps<Props>(), {
count: 0
});
// Access props
console.log(props.title);
console.log(props.count);
// Destructuring loses reactivity - use toRefs
import { toRefs } from 'vue';
const { title, count } = toRefs(props);
</script>
<script setup lang="ts">
// Type-safe emits
const emit = defineEmits<{
update: [value: number];
delete: [];
change: [id: string, value: string];
}>();
function handleUpdate() {
emit('update', 42);
}
function handleChange(id: string, value: string) {
emit('change', id, value);
}
</script>
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0,
validator: (value: number) => value >= 0
},
status: {
type: String as PropType<'active' | 'inactive'>,
default: 'active'
}
});
</script>
<!-- Parent Component -->
<script setup lang="ts">
import { provide, ref } from 'vue';
const theme = ref('dark');
const updateTheme = (newTheme: string) => {
theme.value = newTheme;
};
provide('theme', { theme, updateTheme });
</script>
<!-- Child Component (any depth) -->
<script setup lang="ts">
import { inject } from 'vue';
const themeContext = inject('theme');
// themeContext.theme
// themeContext.updateTheme('light')
</script>
// keys.ts
import type { InjectionKey, Ref } from 'vue';
export interface ThemeContext {
theme: Ref<string>;
updateTheme: (theme: string) => void;
}
export const ThemeKey: InjectionKey<ThemeContext> =
Symbol('theme');
// Provider
<script setup lang="ts">
import { provide, ref } from 'vue';
import { ThemeKey } from './keys';
const theme = ref('dark');
const updateTheme = (newTheme: string) => {
theme.value = newTheme;
};
provide(ThemeKey, { theme, updateTheme });
</script>
// Consumer
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from './keys';
const theme = inject(ThemeKey);
// Fully typed!
</script>
<script setup lang="ts">
import { inject } from 'vue';
const theme = inject('theme', {
theme: ref('light'),
updateTheme: () => {}
});
// Or use factory function for reactive defaults
const config = inject('config', () => reactive({
locale: 'en',
timezone: 'UTC'
}), true); // true = treat as factory
</script>
<script setup lang="ts">
import { ref, computed, type Ref, type ComputedRef } from 'vue';
interface User {
id: number;
name: string;
email: string;
}
interface Props {
userId: number;
}
interface Emits {
(e: 'update', user: User): void;
(e: 'delete', id: number): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const user: Ref<User | null> = ref(null);
const isLoading = ref(false);
const userName: ComputedRef<string> = computed(() =>
user.value?.name ?? 'Unknown'
);
async function loadUser() {
isLoading.value = true;
try {
const response = await fetch(`/api/users/${props.userId}`);
user.value = await response.json();
} finally {
isLoading.value = false;
}
}
function updateUser(updates: Partial<User>) {
if (user.value) {
user.value = { ...user.value, ...updates };
emit('update', user.value);
}
}
</script>
// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue';
export function useLocalStorage<T>(
key: string,
defaultValue: T
): Ref<T> {
const data = ref<T>(defaultValue) as Ref<T>;
// Load from localStorage
const stored = localStorage.getItem(key);
if (stored) {
try {
data.value = JSON.parse(stored);
} catch (e) {
console.error('Failed to parse localStorage', e);
}
}
// Save to localStorage on change
watch(
data,
(newValue) => {
localStorage.setItem(key, JSON.stringify(newValue));
},
{ deep: true }
);
return data;
}
// Usage
const user = useLocalStorage<User>('user', { id: 0, name: '' });
Use vue-composition-api when building modern, production-ready applications that require:
<script setup> syntax - Cleaner, better performance, better typescomposables/ directoryref for primitives, reactive for objects - Unless you need to
replace objectstoRefs - Preserve reactivityonUnmounted for event listeners, timersuse prefix - Follow convention (useCounter, useFetch)toRefs(props).value on refs - Common source of bugs<script setup><script setup lang="ts">
import { reactive, computed } from 'vue';
interface FormData {
name: string;
email: string;
password: string;
}
interface FormErrors {
name?: string;
email?: string;
password?: string;
}
const form = reactive<FormData>({
name: '',
email: '',
password: ''
});
const errors = reactive<FormErrors>({});
const isValid = computed(() =>
Object.keys(errors).length === 0 &&
form.name && form.email && form.password
);
function validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validate() {
if (!form.name) {
errors.name = 'Name is required';
} else {
delete errors.name;
}
if (!validateEmail(form.email)) {
errors.email = 'Invalid email';
} else {
delete errors.email;
}
if (form.password.length < 8) {
errors.password = 'Password must be 8+ characters';
} else {
delete errors.password;
}
}
async function submit() {
validate();
if (!isValid.value) return;
// Submit form
await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(form)
});
}
</script>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
interface Data {
id: number;
title: string;
}
const data = ref<Data[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
async function fetchData() {
loading.value = true;
error.value = null;
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error('Failed to fetch');
data.value = await response.json();
} catch (e) {
error.value = (e as Error).message;
} finally {
loading.value = false;
}
}
onMounted(() => {
fetchData();
});
</script>
<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>
<div v-for="item in data" :key="item.id">
{{ item.title }}
</div>
</div>
</div>
</template>
Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.