vue-composition-api

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vue Composition API

Vue Composition API

Master the Vue 3 Composition API for building scalable, maintainable Vue applications with better code organization and reusability.
掌握Vue 3 Composition API,构建具有更优代码组织性和可复用性的可扩展、可维护Vue应用程序。

Setup Function Fundamentals

Setup函数基础

The
setup()
function is the entry point for using the Composition API:
typescript
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
    };
  }
};
setup()
函数是使用Composition API的入口点:
typescript
import { ref, computed, onMounted } from 'vue';

export default {
  props: ['initialCount'],
  setup(props, context) {
    // props是响应式的
    console.log(props.initialCount);

    // context提供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('组件已挂载');
    });

    // 暴露公共方法
    expose({ increment });

    // 返回值给模板使用
    return {
      count,
      doubled,
      increment
    };
  }
};

Script Setup Syntax

Script Setup语法

Modern Vue 3 uses
<script setup>
for cleaner syntax:
typescript
<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>
现代Vue 3使用
<script setup>
实现更简洁的语法:
typescript
<script setup lang="ts">
import { ref, computed } from 'vue';

// 顶层绑定会自动暴露给模板
const count = ref(0);
const doubled = computed(() => count.value * 2);

function increment() {
  count.value++;
}

// 使用编译器宏定义Props和Emits
interface Props {
  initialCount?: number;
}

const props = withDefaults(defineProps<Props>(), {
  initialCount: 0
});

const emit = defineEmits<{
  update: [value: number];
}>();
</script>

<template>
  <div>
    <p>计数:{{ count }}</p>
    <p>双倍值:{{ doubled }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

Ref vs Reactive - When to Use Each

Ref与Reactive - 各自适用场景

Use Ref For

使用Ref的场景

typescript
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
typescript
import { ref } from 'vue';

// 基本类型
const count = ref(0);
const name = ref('John');
const isActive = ref(true);

// 需要被整体替换的单个对象
const user = ref({ name: 'John', age: 30 });
user.value = { name: 'Jane', age: 25 }; // 有效

// 需要被整体替换的数组
const items = ref([1, 2, 3]);
items.value = [4, 5, 6]; // 有效

Use Reactive For

使用Reactive的场景

typescript
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);
typescript
import { reactive, toRefs } from 'vue';

// 复杂嵌套对象
const state = reactive({
  user: { name: 'John', age: 30 },
  settings: { theme: 'dark', notifications: true },
  posts: []
});

// 相关状态分组
const formState = reactive({
  name: '',
  email: '',
  password: '',
  errors: {}
});

// 转换为ref以支持解构
const { name, email } = toRefs(formState);

Avoid Reactive For

避免使用Reactive的场景

typescript
// 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
typescript
// 错误示例:替换整个reactive对象会丢失响应性
let state = reactive({ count: 0 });
state = reactive({ count: 1 }); // 破坏响应性!

// 正确做法:改用ref
const state = ref({ count: 0 });
state.value = { count: 1 }; // 有效

Computed Properties Patterns

计算属性模式

Basic Computed

基础计算属性

typescript
import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});
typescript
import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

Writable Computed

可写计算属性

typescript
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';
typescript
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(value) {
    const names = value.split(' ');
    firstName.value = names[0] || '';
    lastName.value = names[1] || '';
  }
});

// 现在可以直接设置
fullName.value = 'Jane Smith';

Computed with Complex Logic

包含复杂逻辑的计算属性

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

Watch and WatchEffect

Watch与WatchEffect

Watch - Explicit Dependencies

Watch - 显式依赖

typescript
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 }
);
typescript
import { ref, watch } from 'vue';

const count = ref(0);
const name = ref('');

// 监听单个源
watch(count, (newValue, oldValue) => {
  console.log(`计数从${oldValue}变为${newValue}`);
});

// 监听多个源
watch(
  [count, name],
  ([newCount, newName], [oldCount, oldName]) => {
    console.log('多个值已变更');
  }
);

// 监听响应式对象的属性
const user = reactive({ name: 'John', age: 30 });

watch(
  () => user.name,
  (newName) => {
    console.log(`姓名变更为${newName}`);
  }
);

// 深度监听
watch(
  user,
  (newUser) => {
    console.log('用户信息变更:', newUser);
  },
  { deep: true }
);

WatchEffect - Auto Tracking

WatchEffect - 自动追踪依赖

typescript
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
typescript
import { ref, watchEffect } from 'vue';

const count = ref(0);
const multiplier = ref(2);

// 自动追踪依赖
watchEffect(() => {
  console.log(`结果: ${count.value * multiplier.value}`);
});

// 立即执行,并在依赖变更时重新执行

Advanced Watch Options

高级Watch选项

typescript
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
typescript
const data = ref(null);

watch(
  source,
  (newValue, oldValue) => {
    // 回调逻辑
  },
  {
    immediate: true,      // 立即执行
    deep: true,           // 深度监听对象
    flush: 'post',        // 执行时机: 'pre' | 'post' | 'sync'
    onTrack(e) {          // 调试用
      console.log('已追踪', e);
    },
    onTrigger(e) {        // 调试用
      console.log('已触发', e);
    }
  }
);

// 停止监听
const stop = watch(source, callback);
stop(); // 清理

Lifecycle Hooks in Composition API

Composition API中的生命周期钩子

typescript
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');
    });
  }
};
typescript
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onActivated,
  onDeactivated
} from 'vue';

export default {
  setup() {
    onBeforeMount(() => {
      console.log('挂载前');
    });

    onMounted(() => {
      console.log('已挂载');
      // DOM已可用
      // 设置事件监听器、获取数据
    });

    onBeforeUpdate(() => {
      console.log('更新前');
    });

    onUpdated(() => {
      console.log('已更新');
      // DOM已更新
    });

    onBeforeUnmount(() => {
      console.log('卸载前');
      // 卸载前清理
    });

    onUnmounted(() => {
      console.log('已卸载');
      // 最终清理
    });

    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err, info);
      return false; // 停止传播
    });

    // 适用于被<KeepAlive>包裹的组件
    onActivated(() => {
      console.log('组件已激活');
    });

    onDeactivated(() => {
      console.log('组件已停用');
    });
  }
};

Composables - Reusable Composition Functions

组合式函数 - 可复用的组合逻辑

Simple Composable

简单组合式函数

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

// 使用示例
<script setup lang="ts">
import { useCounter } from '@/composables/useCounter';

const { count, doubled, increment, decrement } = useCounter(10);
</script>

Advanced Composable with Side Effects

包含副作用的高级组合式函数

typescript
// 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>
typescript
// 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('请求失败');
      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
  };
}

// 使用示例
<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>

Composable with Cleanup

包含清理逻辑的组合式函数

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

// 使用示例
<script setup lang="ts">
import { useEventListener } from '@/composables/useEventListener';

useEventListener(window, 'resize', () => {
  console.log('窗口已调整大小');
});
</script>

Props and Emits in Composition API

Composition API中的Props与Emits

TypeScript Props

TypeScript Props

typescript
<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>
typescript
<script setup lang="ts">
interface Props {
  title: string;
  count?: number;
  items: string[];
  user: {
    name: string;
    email: string;
  };
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
});

// 访问props
console.log(props.title);
console.log(props.count);

// 直接解构会丢失响应性 - 使用toRefs
import { toRefs } from 'vue';
const { title, count } = toRefs(props);
</script>

TypeScript Emits

TypeScript Emits

typescript
<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>
typescript
<script setup lang="ts">
// 类型安全的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>

Runtime Props Validation

运行时Props验证

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

Provide and Inject Patterns

Provide与Inject模式

Basic Provide/Inject

基础Provide/Inject

typescript
<!-- 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>
typescript
<!-- 父组件 -->
<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>

<!-- 子组件(任意深度) -->
<script setup lang="ts">
import { inject } from 'vue';

const themeContext = inject('theme');
// themeContext.theme
// themeContext.updateTheme('light')
</script>

Type-Safe Provide/Inject

类型安全的Provide/Inject

typescript
// 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>
typescript
// 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');

// 提供者
<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>

// 消费者
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from './keys';

const theme = inject(ThemeKey);
// 完全类型化!
</script>

Provide with Default Values

带默认值的Provide

typescript
<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>
typescript
<script setup lang="ts">
import { inject } from 'vue';

const theme = inject('theme', {
  theme: ref('light'),
  updateTheme: () => {}
});

// 或使用工厂函数创建响应式默认值
const config = inject('config', () => reactive({
  locale: 'en',
  timezone: 'UTC'
}), true); // true = 视为工厂函数
</script>

TypeScript with Composition API

Composition API与TypeScript

Component with Full Types

全类型组件

typescript
<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>
typescript
<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 ?? '未知'
);

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>

Generic Composables

泛型组合式函数

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

  // 从localStorage加载
  const stored = localStorage.getItem(key);
  if (stored) {
    try {
      data.value = JSON.parse(stored);
    } catch (e) {
      console.error('解析localStorage失败', e);
    }
  }

  // 变更时保存到localStorage
  watch(
    data,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true }
  );

  return data;
}

// 使用示例
const user = useLocalStorage<User>('user', { id: 0, name: '' });

When to Use This Skill

何时使用该技能

Use vue-composition-api when building modern, production-ready applications that require:
  • Complex component logic that benefits from better organization
  • Reusable logic across multiple components (composables)
  • Better TypeScript integration and type inference
  • Fine-grained reactivity control
  • Large-scale applications requiring maintainability
  • Migration from Vue 2 Options API to Vue 3
  • Sharing stateful logic without mixins
在构建现代、生产级应用程序时使用vue-composition-api,尤其是当应用需要:
  • 可从更优代码组织中获益的复杂组件逻辑
  • 可在多个组件间复用的逻辑(组合式函数)
  • 更好的TypeScript集成与类型推断
  • 细粒度的响应性控制
  • 可维护的大规模应用程序
  • 从Vue 2 Options API迁移到Vue 3
  • 无需混入即可共享有状态逻辑

Vue-Specific Best Practices

Vue专属最佳实践

  1. Prefer
    <script setup>
    syntax
    - Cleaner, better performance, better types
  2. Use composables for reusable logic - Extract to
    composables/
    directory
  3. Use
    ref
    for primitives,
    reactive
    for objects
    - Unless you need to replace objects
  4. Always use TypeScript - Better DX and fewer runtime errors
  5. Destructure reactive objects with
    toRefs
    - Preserve reactivity
  6. Use computed for derived state - Not methods in templates
  7. Cleanup side effects - Use
    onUnmounted
    for event listeners, timers
  8. Keep components focused - Extract complex logic to composables
  9. Use provide/inject for deep prop passing - Avoid prop drilling
  10. Name composables with
    use
    prefix
    - Follow convention (useCounter, useFetch)
  1. 优先使用
    <script setup>
    语法
    - 更简洁、性能更优、类型支持更好
  2. 使用组合式函数实现可复用逻辑 - 提取到
    composables/
    目录
  3. 基本类型用ref,对象用reactive - 除非你需要替换整个对象
  4. 始终使用TypeScript - 更好的开发体验,更少的运行时错误
  5. 用toRefs解构响应式对象 - 保留响应性
  6. 使用计算属性处理派生状态 - 而非模板中的方法
  7. 清理副作用 - 使用
    onUnmounted
    处理事件监听器、定时器
  8. 保持组件聚焦 - 将复杂逻辑提取到组合式函数
  9. 使用provide/inject进行深层属性传递 - 避免属性透传
  10. 组合式函数以
    use
    前缀命名
    - 遵循约定(useCounter、useFetch)

Vue-Specific Pitfalls

Vue常见陷阱

  1. Destructuring props directly - Loses reactivity, use
    toRefs(props)
  2. Forgetting
    .value
    on refs
    - Common source of bugs
  3. Mutating props - Props are readonly, emit events instead
  4. Using reactive() for entire state - Can't replace, use ref for root
  5. Not cleaning up watchers - Memory leaks, store stop handle
  6. Accessing refs before mount - DOM refs are null in setup
  7. Overusing reactive() - Use ref for simple values
  8. Not using computed for derived state - Recalculates on every render
  9. Forgetting to return from setup() - Without
    <script setup>
  10. Mixing Options API and Composition API - Confusing, pick one
  1. 直接解构props - 丢失响应性,改用
    toRefs(props)
  2. 忘记ref的
    .value
    - 常见错误来源
  3. 修改props - props是只读的,应触发事件通知父组件
  4. 用reactive()管理整个状态 - 无法整体替换,根状态用ref
  5. 未清理监听器 - 内存泄漏,保存stop句柄
  6. 挂载前访问ref - setup中DOM ref为null
  7. 过度使用reactive() - 简单值用ref
  8. 未用计算属性处理派生状态 - 每次渲染都会重新计算
  9. setup()未返回值 - 未使用
    <script setup>
    时需注意
  10. 混合Options API与Composition API - 易混淆,选择一种即可

Common Patterns

常见模式

Form Handling

表单处理

typescript
<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>
typescript
<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 = '姓名为必填项';
  } else {
    delete errors.name;
  }

  if (!validateEmail(form.email)) {
    errors.email = '无效邮箱';
  } else {
    delete errors.email;
  }

  if (form.password.length < 8) {
    errors.password = '密码长度至少8位';
  } else {
    delete errors.password;
  }
}

async function submit() {
  validate();
  if (!isValid.value) return;

  // 提交表单
  await fetch('/api/register', {
    method: 'POST',
    body: JSON.stringify(form)
  });
}
</script>

Async Data Loading

异步数据加载

typescript
<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>
typescript
<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('请求失败');
    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">加载中...</div>
    <div v-else-if="error">错误:{{ error }}</div>
    <div v-else>
      <div v-for="item in data" :key="item.id">
        {{ item.title }}
      </div>
    </div>
  </div>
</template>

Resources

资源