---
description: Creates Vue 3 composable functions (组合式函数) for state management, data fetching, and reusable logic. Use when building shared state, encapsulating side effects, or extracting reusable logic from components.
---

# Vue 3 Composable Creator

Create composable functions following these conventions:

## Naming

- File: `src/composables/use{Name}.js` (e.g., `useTodo.js`, `useTheme.js`)
- Export: named function `export function use{Name}()`
- Helper exports: named exports for constants (`export const themes = [...]`)

## Architecture Pattern

### Module-level State (Shared Singleton)

Use module-level `ref()` for state shared across components. This creates a singleton pattern where all components using the composable share the same state:

```js
import { ref, computed, watch } from 'vue'

const STORAGE_KEY = 'app_data'

// Module-level: shared across all consumers
const items = ref(load())

// Persistence: debounced watch
let saveTimer = null
const debouncedSave = (data) => {
  clearTimeout(saveTimer)
  saveTimer = setTimeout(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
  }, 300)
}

watch(items, (val) => debouncedSave(val), { deep: true })

export function useItems() {
  const filtered = computed(() => {
    // derive from items.value
  })

  const addItem = (text) => {
    items.value.push({ id: genId(), text })
  }

  return { items, filtered, addItem }
}
```

### When to Use Each Pattern

| Pattern | Use Case | State Scope |
|---------|----------|-------------|
| Module-level `ref()` | Shared state (todos, theme, cart) | All components |
| Inside `useXxx()` | Component-local state | Single component instance |
| `computed()` | Derived/filtered data | Read-only view |

## Rules

1. **No external state libraries** — Use Vue 3 reactivity primitives (`ref`, `computed`, `watch`)
2. **Debounced persistence** — Use `watch()` with a debounced save to `localStorage`, not per-action saves
3. **Return object** — Always return a destructurable object with reactive refs and methods
4. **Computed over methods** — Use `computed()` for derived data instead of methods
5. **Data migration** — Include a `migrate()` function for backward compatibility when schema changes
6. **Error handling** — Wrap `JSON.parse` in try/catch with sensible defaults

## Common Composable Templates

### Data Management

```js
import { ref, computed, watch } from 'vue'

const STORAGE_KEY = 'app_items'

const load = () => {
  try {
    return JSON.parse(localStorage.getItem(STORAGE_KEY)) || []
  } catch {
    return []
  }
}

const items = ref(load())
const filter = ref('all')

let saveTimer = null
watch(items, (val) => {
  clearTimeout(saveTimer)
  saveTimer = setTimeout(() => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(val))
  }, 300)
}, { deep: true })

export function useItems() {
  const filtered = computed(() => {
    if (filter.value === 'active') return items.value.filter(i => !i.done)
    if (filter.value === 'completed') return items.value.filter(i => i.done)
    return items.value
  })

  const stats = computed(() => ({
    total: items.value.length,
    done: items.value.filter(i => i.done).length,
  }))

  const add = (text) => {
    items.value.unshift({ id: Date.now().toString(36), text, done: false })
  }

  const remove = (id) => {
    items.value = items.value.filter(i => i.id !== id)
  }

  return { items, filter, filtered, stats, add, remove }
}
```

### Theme Management

```js
import { ref, watch } from 'vue'

export const themes = [
  { key: 'dark', color: '#6366F1', label: 'Dark' },
  { key: 'light', color: '#D97706', label: 'Light' },
]

const current = ref(localStorage.getItem('app_theme') || 'dark')

watch(current, (val) => {
  document.documentElement.setAttribute('data-theme', val)
  localStorage.setItem('app_theme', val)
}, { immediate: true })

export function useTheme() {
  const setTheme = (key) => { current.value = key }
  return { current, setTheme, themes }
}
```

### Toast/Notification

```js
import { ref } from 'vue'

const toasts = ref([])
let toastId = 0

export function useToast() {
  const show = (message, type = 'info', duration = 3000) => {
    const id = ++toastId
    toasts.value.push({ id, message, type })
    setTimeout(() => {
      toasts.value = toasts.value.filter(t => t.id !== id)
    }, duration)
  }

  return { toasts, show }
}
```
