@pythoughts/vue-skills-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/vue-ai-apps/SKILL.md +51 -0
- package/skills/vue-ai-apps/references/error-handling-and-abort.md +87 -0
- package/skills/vue-ai-apps/references/streaming-chat-ui.md +101 -0
- package/skills/vue-ai-apps/references/structured-output.md +90 -0
- package/skills/vue-ai-apps/references/tool-calling.md +88 -0
- package/skills/vue-best-practices/SKILL.md +8 -1
- package/skills/vue-best-practices/references/reactive-props-destructure.md +80 -0
- package/skills/vue-best-practices/references/vapor-mode.md +65 -0
- package/skills/vue-best-practices/references/vue-3-5-helpers.md +91 -0
- package/skills/create-adaptable-composable/SKILL.md +0 -76
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pythoughts/vue-skills-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server exposing the Vue 3 best-practice skills so any MCP coding agent can fetch them automatically on Vue work.",
|
|
6
6
|
"author": "Mohamed Elkholy (elkaix)",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vue-ai-apps
|
|
3
|
+
description: "Building AI/LLM and agent apps with Vue 3 and Nuxt: streaming chat UIs, the Vercel AI SDK (`ai` + `@ai-sdk/vue`), `useChat`, tool calling, structured output, and abort/error handling. Load for AI chatbots, assistant UIs, LLM streaming, or agent frontends in Vue or Nuxt."
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
license: MIT
|
|
6
|
+
author: github.com/Pythoughts-labs
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vue AI Apps Workflow
|
|
10
|
+
|
|
11
|
+
Building the **Vue/Nuxt front end** for LLM and agent features with the Vercel AI SDK. This skill covers the Vue-specific integration; the model layer (`streamText`, `tool`, providers) is shown as shape only — follow the AI SDK docs for the current core API, which moves between minor versions.
|
|
12
|
+
|
|
13
|
+
Assumes the foundations in `vue-best-practices` (Composition API, `<script setup lang="ts">`, composables). Load that skill for component structure and reactivity.
|
|
14
|
+
|
|
15
|
+
## Core Principles
|
|
16
|
+
|
|
17
|
+
- **Keys never reach the client.** The provider API key lives on the server. The browser talks to *your* endpoint, never to the model provider directly.
|
|
18
|
+
- **Server streams, client consumes.** A server route runs `streamText`/`streamObject` and returns a stream; the Vue component renders it incrementally. Never block on the full response.
|
|
19
|
+
- **Render message *parts*, not a content string.** AI SDK v5 messages are `UIMessage[]` made of typed `parts` (text, tool calls, reasoning). Iterate `message.parts`.
|
|
20
|
+
- **Drive UI from `status`, not a boolean.** Disabled inputs, spinners, and the stop button key off the `status` state machine.
|
|
21
|
+
|
|
22
|
+
## 1) Confirm the stack (required)
|
|
23
|
+
|
|
24
|
+
- Packages: `ai` (core), `@ai-sdk/vue` (composables), a provider (`@ai-sdk/openai`, `@ai-sdk/anthropic`, …), `zod` for tool/object schemas.
|
|
25
|
+
- `useChat` from `@ai-sdk/vue` works against **any** streaming backend. The server snippets here use **Nuxt/Nitro** (`defineEventHandler`, `readBody`); for a non-Nuxt backend, keep the same AI SDK calls in your own route handler.
|
|
26
|
+
- AI SDK **v5** is assumed (messages have `parts`; the hook does not manage input; `status` replaces `isLoading`; tool schemas use `inputSchema`). If the project is on v4, these APIs differ — check the installed version first.
|
|
27
|
+
|
|
28
|
+
## 2) Build the streaming chat UI (required for chat features)
|
|
29
|
+
|
|
30
|
+
- Reference: [streaming-chat-ui](references/streaming-chat-ui.md)
|
|
31
|
+
- Server route streams with `streamText` + `toUIMessageStreamResponse()`.
|
|
32
|
+
- Client: `useChat()` returns Vue refs. Own your own `input` ref; call `sendMessage({ text })`. Render `message.parts`.
|
|
33
|
+
|
|
34
|
+
## 3) Add capabilities only when the feature needs them
|
|
35
|
+
|
|
36
|
+
- **Tool calling** (agent actions, function calling) → [tool-calling](references/tool-calling.md)
|
|
37
|
+
- **Structured output** (stream a typed object, not chat) → [structured-output](references/structured-output.md)
|
|
38
|
+
|
|
39
|
+
## 4) Handle abort and errors (required before shipping)
|
|
40
|
+
|
|
41
|
+
- Reference: [error-handling-and-abort](references/error-handling-and-abort.md)
|
|
42
|
+
- Wire `stop()`, the `error` ref, and `onError`. A streaming UI without an abort path is not done.
|
|
43
|
+
|
|
44
|
+
## 5) Final self-check
|
|
45
|
+
|
|
46
|
+
- Provider key is server-only; no key or provider call in client code.
|
|
47
|
+
- Component renders `message.parts`, not `message.content`.
|
|
48
|
+
- Input is disabled and a stop button shows while `status` is `submitted`/`streaming`.
|
|
49
|
+
- Errors are surfaced to the user and retryable (`clearError()` + `regenerate()`).
|
|
50
|
+
- Tool/object schemas validate input with `zod`.
|
|
51
|
+
- Core AI SDK calls were checked against the installed SDK version, not assumed.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Abort and Error Handling for AI Streams (AI SDK v5)
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: A streaming UI with no stop button and no error surface leaves users stuck on hung or failed requests
|
|
5
|
+
type: best-practice
|
|
6
|
+
tags: [vue3, nuxt, ai-sdk, useChat, abort, error-handling, streaming]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Abort and Error Handling for AI Streams (AI SDK v5)
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - LLM streams are long-running and can fail mid-response. `useChat` exposes everything needed — `status`, `stop()`, `error`, `clearError()`, `regenerate()`, and an `onError` option — but none of it is wired up by default. A chat UI is not shippable until the user can stop a runaway generation and recover from an error.
|
|
12
|
+
|
|
13
|
+
## Task Checklist
|
|
14
|
+
|
|
15
|
+
- [ ] Show a **Stop** button while `status` is `submitted` or `streaming`, calling `stop()`
|
|
16
|
+
- [ ] Render the `error` ref when present
|
|
17
|
+
- [ ] Offer recovery: `clearError()` then `regenerate()`
|
|
18
|
+
- [ ] Pass `onError` for logging/toasts (never log the provider key or full request)
|
|
19
|
+
- [ ] Disable the send control unless `status === 'ready'`
|
|
20
|
+
|
|
21
|
+
**Incorrect - no abort, no error surface:**
|
|
22
|
+
```vue
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
const { messages, sendMessage } = useChat() // ❌ ignores status/error/stop
|
|
25
|
+
</script>
|
|
26
|
+
<!-- user cannot cancel a long stream and never sees failures -->
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Correct:**
|
|
30
|
+
```vue
|
|
31
|
+
<script setup lang="ts">
|
|
32
|
+
import { ref } from 'vue'
|
|
33
|
+
import { useChat } from '@ai-sdk/vue'
|
|
34
|
+
|
|
35
|
+
const input = ref('')
|
|
36
|
+
const { messages, sendMessage, status, error, stop, clearError, regenerate } = useChat({
|
|
37
|
+
onError(err) {
|
|
38
|
+
// surface to logging/toast; keep payloads out of logs
|
|
39
|
+
console.error('chat stream failed:', err.message)
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
function send() {
|
|
44
|
+
const text = input.value.trim()
|
|
45
|
+
if (!text || status.value !== 'ready') return
|
|
46
|
+
sendMessage({ text })
|
|
47
|
+
input.value = ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function retry() {
|
|
51
|
+
clearError()
|
|
52
|
+
regenerate()
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<!-- messages render here (see streaming-chat-ui) -->
|
|
58
|
+
|
|
59
|
+
<div v-if="error" role="alert">
|
|
60
|
+
Something went wrong: {{ error.message }}
|
|
61
|
+
<button @click="retry">Retry</button>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<form @submit.prevent="send">
|
|
65
|
+
<input v-model="input" :disabled="status !== 'ready'" />
|
|
66
|
+
<button
|
|
67
|
+
v-if="status === 'submitted' || status === 'streaming'"
|
|
68
|
+
type="button"
|
|
69
|
+
@click="stop"
|
|
70
|
+
>
|
|
71
|
+
Stop
|
|
72
|
+
</button>
|
|
73
|
+
<button v-else :disabled="status !== 'ready'">Send</button>
|
|
74
|
+
</form>
|
|
75
|
+
</template>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Notes
|
|
79
|
+
|
|
80
|
+
- `stop()` aborts the in-flight request; the partial assistant message stays in `messages`.
|
|
81
|
+
- `regenerate()` (v5; replaces v4 `reload`) re-runs the last user turn — pair with `clearError()` after a failure.
|
|
82
|
+
- `useObject` does not expose `status`; it uses `isLoading` + `error` + `stop()` instead.
|
|
83
|
+
- Surface a friendly message to users; keep raw errors, request bodies, and keys out of client logs.
|
|
84
|
+
|
|
85
|
+
## Reference
|
|
86
|
+
- [AI SDK — useChat reference](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat)
|
|
87
|
+
- [AI SDK — Chatbot: status & stop](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Streaming Chat UI with useChat (AI SDK v5)
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Rendering message.content or managing input inside the hook is the v4 API and silently breaks in v5
|
|
5
|
+
type: capability
|
|
6
|
+
tags: [vue3, nuxt, ai-sdk, useChat, streaming, llm, chat]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Streaming Chat UI with useChat (AI SDK v5)
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - In AI SDK v5 the Vue `useChat` composable no longer manages the input field, exposes `status` instead of `isLoading`, and returns messages as `UIMessage[]` built from typed `parts`. Code written for v4 (`input`, `handleSubmit`, `message.content`, `isLoading`) compiles but renders nothing useful.
|
|
12
|
+
|
|
13
|
+
The provider API key must stay on the server. The browser calls your own route; your route calls the model.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Keep the provider key and `streamText` call in a server route, never in the component
|
|
18
|
+
- [ ] Import `useChat` from `@ai-sdk/vue` (not `@ai-sdk/react`)
|
|
19
|
+
- [ ] Own a local `input` ref; send with `sendMessage({ text })`
|
|
20
|
+
- [ ] Render `message.parts`, branching on `part.type`
|
|
21
|
+
- [ ] Disable the input while `status` is `submitted` or `streaming`
|
|
22
|
+
|
|
23
|
+
**Incorrect - v4-style usage, broken in v5:**
|
|
24
|
+
```vue
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { useChat } from '@ai-sdk/vue'
|
|
27
|
+
// ❌ input / handleSubmit / isLoading no longer exist on the hook in v5
|
|
28
|
+
const { messages, input, handleSubmit, isLoading } = useChat()
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div v-for="m in messages" :key="m.id">
|
|
33
|
+
{{ m.content }} <!-- ❌ v5 messages have no `content`; they have `parts` -->
|
|
34
|
+
</div>
|
|
35
|
+
<form @submit.prevent="handleSubmit">
|
|
36
|
+
<input v-model="input" :disabled="isLoading" />
|
|
37
|
+
</form>
|
|
38
|
+
</template>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Correct - server route (Nuxt/Nitro), `server/api/chat.ts`:**
|
|
42
|
+
```ts
|
|
43
|
+
import { streamText, convertToModelMessages, type UIMessage } from 'ai'
|
|
44
|
+
import { openai } from '@ai-sdk/openai' // key read from env on the server
|
|
45
|
+
|
|
46
|
+
export default defineEventHandler(async (event) => {
|
|
47
|
+
const { messages } = await readBody<{ messages: UIMessage[] }>(event)
|
|
48
|
+
|
|
49
|
+
const result = streamText({
|
|
50
|
+
model: openai('gpt-4o'),
|
|
51
|
+
messages: convertToModelMessages(messages),
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Standard v5 streaming response. (The Nuxt template also shows a
|
|
55
|
+
// gateway-wrapped form with toUIMessageStream — both are valid.)
|
|
56
|
+
return result.toUIMessageStreamResponse()
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Correct - component, `pages/index.vue`:**
|
|
61
|
+
```vue
|
|
62
|
+
<script setup lang="ts">
|
|
63
|
+
import { ref } from 'vue'
|
|
64
|
+
import { useChat } from '@ai-sdk/vue'
|
|
65
|
+
|
|
66
|
+
const input = ref('')
|
|
67
|
+
const { messages, sendMessage, status } = useChat()
|
|
68
|
+
|
|
69
|
+
function send() {
|
|
70
|
+
const text = input.value.trim()
|
|
71
|
+
if (!text || status.value !== 'ready') return
|
|
72
|
+
sendMessage({ text })
|
|
73
|
+
input.value = ''
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<div v-for="m in messages" :key="m.id">
|
|
79
|
+
<strong>{{ m.role === 'user' ? 'You' : 'AI' }}:</strong>
|
|
80
|
+
<template v-for="(part, i) in m.parts" :key="i">
|
|
81
|
+
<span v-if="part.type === 'text'">{{ part.text }}</span>
|
|
82
|
+
</template>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<form @submit.prevent="send">
|
|
86
|
+
<input v-model="input" :disabled="status !== 'ready'" placeholder="Ask something…" />
|
|
87
|
+
<button :disabled="status !== 'ready'">Send</button>
|
|
88
|
+
</form>
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Notes
|
|
93
|
+
|
|
94
|
+
- `useChat` returns Vue refs (`messages.value`, `status.value`); templates unwrap them automatically.
|
|
95
|
+
- `status` values: `'submitted'` (sent, awaiting first token) → `'streaming'` (receiving) → `'ready'` (done) → `'error'`. Treat `submitted` + `streaming` as busy.
|
|
96
|
+
- `sendMessage` also accepts files/attachments and per-call options; `text` is the common case.
|
|
97
|
+
- The core `streamText` signature can change between AI SDK minors — verify against the installed version rather than copying blindly.
|
|
98
|
+
|
|
99
|
+
## Reference
|
|
100
|
+
- [AI SDK — Nuxt quickstart](https://ai-sdk.dev/docs/getting-started/nuxt)
|
|
101
|
+
- [AI SDK — useChat reference](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Streaming Structured Output in Vue (AI SDK v5)
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Use streamObject + useObject for typed data; the object streams in partial, so the UI must tolerate undefined fields
|
|
5
|
+
type: capability
|
|
6
|
+
tags: [vue3, nuxt, ai-sdk, streamObject, useObject, structured-output, zod]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Streaming Structured Output in Vue (AI SDK v5)
|
|
10
|
+
|
|
11
|
+
**Impact: MEDIUM** - When the model should return a typed object (a recipe, a form, an extraction) rather than chat, use `streamObject` on the server and the `useObject` composable on the client. The object arrives **incrementally as a deep-partial**, so every field can be `undefined` mid-stream. Templates must guard with optional chaining and `v-if`, or they throw while streaming.
|
|
12
|
+
|
|
13
|
+
`useObject` uses its own surface (`object`, `submit`, `isLoading`) — it does **not** share `useChat`'s `status`/`sendMessage` API.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Use `streamObject` server-side with a `zod` `schema`
|
|
18
|
+
- [ ] Return `result.toTextStreamResponse()`
|
|
19
|
+
- [ ] Drive the UI from `useObject`'s `object`, `submit`, `isLoading`
|
|
20
|
+
- [ ] Treat every field of `object` as possibly `undefined` while streaming
|
|
21
|
+
- [ ] Verify the exact `useObject` export name in your installed `@ai-sdk/vue`
|
|
22
|
+
|
|
23
|
+
**Incorrect - assuming the object is complete:**
|
|
24
|
+
```vue
|
|
25
|
+
<!-- ❌ object and its fields are undefined until the stream fills them -->
|
|
26
|
+
<li v-for="step in object.recipe.steps">{{ step }}</li>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Correct - server route, `server/api/recipe.ts`:**
|
|
30
|
+
```ts
|
|
31
|
+
import { streamObject } from 'ai'
|
|
32
|
+
import { openai } from '@ai-sdk/openai'
|
|
33
|
+
import { z } from 'zod'
|
|
34
|
+
|
|
35
|
+
export const recipeSchema = z.object({
|
|
36
|
+
recipe: z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
steps: z.array(z.string()),
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export default defineEventHandler(async (event) => {
|
|
43
|
+
const { dish } = await readBody<{ dish: string }>(event)
|
|
44
|
+
|
|
45
|
+
const result = streamObject({
|
|
46
|
+
model: openai('gpt-4o'),
|
|
47
|
+
schema: recipeSchema,
|
|
48
|
+
prompt: `Generate a recipe for ${dish}`,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return result.toTextStreamResponse()
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Correct - component:**
|
|
56
|
+
```vue
|
|
57
|
+
<script setup lang="ts">
|
|
58
|
+
// Verify the export name against the installed @ai-sdk/vue:
|
|
59
|
+
// it is exposed as `useObject` (cross-framework convention aliases
|
|
60
|
+
// it as `experimental_useObject`).
|
|
61
|
+
import { useObject } from '@ai-sdk/vue'
|
|
62
|
+
import { z } from 'zod'
|
|
63
|
+
|
|
64
|
+
const schema = z.object({
|
|
65
|
+
recipe: z.object({ name: z.string(), steps: z.array(z.string()) }),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const { object, submit, isLoading } = useObject({ api: '/api/recipe', schema })
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<button :disabled="isLoading" @click="submit({ dish: 'pad thai' })">Generate</button>
|
|
73
|
+
|
|
74
|
+
<!-- guard every level: partial object during streaming -->
|
|
75
|
+
<h2 v-if="object?.recipe?.name">{{ object.recipe.name }}</h2>
|
|
76
|
+
<ol>
|
|
77
|
+
<li v-for="(step, i) in object?.recipe?.steps ?? []" :key="i">{{ step }}</li>
|
|
78
|
+
</ol>
|
|
79
|
+
</template>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- `useObject` is experimental and available for React, Svelte, and **Vue**. The reference docs page only prints the React import, so confirm the Vue export name (`useObject` vs `experimental_useObject`) in `node_modules/@ai-sdk/vue` for your version.
|
|
85
|
+
- `object` is typed as `DeepPartial<Schema>` — TypeScript already forces the optional-chaining discipline above.
|
|
86
|
+
- For free-form text streaming use `useChat` (or `useCompletion`); reach for `useObject` only when you need a validated shape.
|
|
87
|
+
|
|
88
|
+
## Reference
|
|
89
|
+
- [AI SDK — useObject reference](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-object)
|
|
90
|
+
- [AI SDK — streamObject](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-object)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tool Calling in Vue Chat UIs (AI SDK v5)
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Tool calls arrive as typed message parts with a state machine; ignoring the state renders blank or stale UI
|
|
5
|
+
type: capability
|
|
6
|
+
tags: [vue3, nuxt, ai-sdk, tools, function-calling, agents, useChat]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Tool Calling in Vue Chat UIs (AI SDK v5)
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - When the model calls a tool, the result surfaces in the chat as a `parts` entry of type `tool-<name>` with a `state` field. The Vue UI must branch on `part.state` (`input-streaming` → `input-available` → `output-available` / `output-error`) to show progress and results. Rendering the part without checking `state` shows nothing while the tool runs, then throws when accessing `part.output` too early.
|
|
12
|
+
|
|
13
|
+
Multi-step agent loops (call tool → feed result back → answer) require `stopWhen` on the server; without it the model stops after the first tool call.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Define tools server-side with `tool({ description, inputSchema: z.object(...), execute })`
|
|
18
|
+
- [ ] Use `inputSchema` (v5), not `parameters` (v4)
|
|
19
|
+
- [ ] Allow multiple steps with `stopWhen: stepCountIs(n)` for agent behavior
|
|
20
|
+
- [ ] In the template, branch tool parts on `part.state`
|
|
21
|
+
- [ ] Access `part.input` / `part.output` only in the matching state
|
|
22
|
+
|
|
23
|
+
**Incorrect - v4 keys + no state handling:**
|
|
24
|
+
```ts
|
|
25
|
+
// ❌ `parameters` and `maxSteps` are v4; renamed in v5
|
|
26
|
+
tool({ parameters: z.object({ city: z.string() }), execute })
|
|
27
|
+
streamText({ /* ... */ tools, maxSteps: 5 })
|
|
28
|
+
```
|
|
29
|
+
```vue
|
|
30
|
+
<!-- ❌ reads output before the tool has produced it -->
|
|
31
|
+
<div v-if="part.type === 'tool-getWeather'">{{ part.output.tempC }}</div>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct - server route, `server/api/chat.ts`:**
|
|
35
|
+
```ts
|
|
36
|
+
import { streamText, tool, convertToModelMessages, stepCountIs, type UIMessage } from 'ai'
|
|
37
|
+
import { openai } from '@ai-sdk/openai'
|
|
38
|
+
import { z } from 'zod'
|
|
39
|
+
|
|
40
|
+
export default defineEventHandler(async (event) => {
|
|
41
|
+
const { messages } = await readBody<{ messages: UIMessage[] }>(event)
|
|
42
|
+
|
|
43
|
+
const result = streamText({
|
|
44
|
+
model: openai('gpt-4o'),
|
|
45
|
+
messages: convertToModelMessages(messages),
|
|
46
|
+
stopWhen: stepCountIs(5), // let the model use a tool then answer
|
|
47
|
+
tools: {
|
|
48
|
+
getWeather: tool({
|
|
49
|
+
description: 'Get the current weather for a city',
|
|
50
|
+
inputSchema: z.object({ city: z.string() }),
|
|
51
|
+
execute: async ({ city }) => ({ city, tempC: 21 }),
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return result.toUIMessageStreamResponse()
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Correct - rendering the tool part:**
|
|
61
|
+
```vue
|
|
62
|
+
<template v-for="(part, i) in m.parts" :key="i">
|
|
63
|
+
<span v-if="part.type === 'text'">{{ part.text }}</span>
|
|
64
|
+
|
|
65
|
+
<!-- a tool named `getWeather` produces parts of type `tool-getWeather` -->
|
|
66
|
+
<template v-else-if="part.type === 'tool-getWeather'">
|
|
67
|
+
<div v-if="part.state === 'input-streaming'">Preparing request…</div>
|
|
68
|
+
<div v-else-if="part.state === 'input-available'">
|
|
69
|
+
Fetching weather for {{ part.input.city }}…
|
|
70
|
+
</div>
|
|
71
|
+
<div v-else-if="part.state === 'output-available'">
|
|
72
|
+
{{ part.output.city }}: {{ part.output.tempC }}°C
|
|
73
|
+
</div>
|
|
74
|
+
<div v-else-if="part.state === 'output-error'">Error: {{ part.errorText }}</div>
|
|
75
|
+
</template>
|
|
76
|
+
</template>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Notes
|
|
80
|
+
|
|
81
|
+
- Part type follows the pattern `tool-${toolName}`; dynamically registered tools use type `dynamic-tool` with `part.toolName`.
|
|
82
|
+
- Useful accessors: `part.input`, `part.output`, `part.toolCallId`, `part.errorText`.
|
|
83
|
+
- **Client-side tools** (a `tool()` with no `execute`) are fulfilled from the UI by calling `addToolResult({ tool, toolCallId, output })` returned by `useChat`.
|
|
84
|
+
- `stepCountIs` / `stopWhen` and the `tool()` signature are core AI SDK API — confirm against the installed version.
|
|
85
|
+
|
|
86
|
+
## Reference
|
|
87
|
+
- [AI SDK — Chatbot tool usage](https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-tool-usage)
|
|
88
|
+
- [AI SDK — tool() / stopWhen](https://ai-sdk.dev/docs/reference/ai-sdk-core/step-count-is)
|
|
@@ -4,7 +4,7 @@ description: MUST be used for Vue.js tasks. Strongly recommends Composition API
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: github.com/Pythoughts-labs
|
|
7
|
-
version: "18.
|
|
7
|
+
version: "18.2.0"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Vue Best Practices Workflow
|
|
@@ -102,6 +102,11 @@ Entry/root and route view rule:
|
|
|
102
102
|
- Keep composable APIs small, typed, and predictable.
|
|
103
103
|
- Separate feature logic from presentational components.
|
|
104
104
|
|
|
105
|
+
### Vue 3.5+ APIs (apply on Vue 3.5 or newer)
|
|
106
|
+
|
|
107
|
+
- Prefer destructure defaults over `withDefaults()`; mind the getter-boundary rule -> [reactive-props-destructure](references/reactive-props-destructure.md)
|
|
108
|
+
- Use 3.5 built-ins before hand-rolling: `useId`, `onWatcherCleanup`, `<Teleport defer>`, lazy hydration -> [vue-3-5-helpers](references/vue-3-5-helpers.md)
|
|
109
|
+
|
|
105
110
|
## 3) Consider optional features only when requirements call for them
|
|
106
111
|
|
|
107
112
|
### 3.1 Standard optional features
|
|
@@ -138,6 +143,8 @@ Performance work is a post-functionality pass. Do not optimize before core behav
|
|
|
138
143
|
- Over-abstraction in hot list paths -> [perf-avoid-component-abstraction-in-lists](references/perf-avoid-component-abstraction-in-lists.md)
|
|
139
144
|
- Expensive updates triggered too often -> [updated-hook-performance](references/updated-hook-performance.md)
|
|
140
145
|
|
|
146
|
+
Experimental: a measured rendering hotspot may justify Vapor Mode (Vue 3.6, opt-in, unstable) -> [vapor-mode](references/vapor-mode.md). Do not enable by default.
|
|
147
|
+
|
|
141
148
|
## 5) Final self-check before finishing
|
|
142
149
|
|
|
143
150
|
- Core behavior works and matches requirements.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Reactive Props Destructure (Vue 3.5+)
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: Destructured props lose reactivity when passed across a function boundary, silently breaking watchers and composables
|
|
5
|
+
type: best-practice
|
|
6
|
+
tags: [vue3, vue35, props, defineProps, reactivity, composition-api, withDefaults]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Reactive Props Destructure (Vue 3.5+)
|
|
10
|
+
|
|
11
|
+
**Impact: HIGH** - Since Vue 3.5 (stable, on by default), destructuring `defineProps()` keeps reactivity and lets you declare defaults with plain JS syntax — replacing `withDefaults()`. The compiler rewrites each destructured access back to `props.x`. The catch: a destructured prop is only reactive when accessed *inside* a reactive scope (template, `computed`, `watchEffect`). The moment you pass the bare variable **into a function** — `watch(count, …)`, a composable, `toRef(count)` — you pass a snapshot value, and reactivity is lost.
|
|
12
|
+
|
|
13
|
+
Vue's compiler warns when it detects a destructured prop passed directly into a function call, but the fix must be applied for the code to work.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Use destructure defaults instead of `withDefaults()` in Vue 3.5+
|
|
18
|
+
- [ ] Access destructured props directly in templates, `computed`, and `watchEffect`
|
|
19
|
+
- [ ] When passing a prop across a function boundary, wrap it in a getter `() => prop`
|
|
20
|
+
- [ ] Watch a destructured prop with `watch(() => prop, …)`, never `watch(prop, …)`
|
|
21
|
+
- [ ] In composables, accept `() => T` and read it with `toValue()`
|
|
22
|
+
|
|
23
|
+
**Incorrect - reactivity lost at the function boundary:**
|
|
24
|
+
```vue
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { watch } from 'vue'
|
|
27
|
+
import { useFetch } from './useFetch'
|
|
28
|
+
|
|
29
|
+
const { id, count = 0 } = defineProps<{ id: number; count?: number }>()
|
|
30
|
+
|
|
31
|
+
// ❌ passes the current number, not a reactive source — never re-runs
|
|
32
|
+
watch(id, () => reload())
|
|
33
|
+
|
|
34
|
+
// ❌ composable receives a one-time value, not a live source
|
|
35
|
+
const { data } = useFetch(id)
|
|
36
|
+
</script>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Correct - wrap in a getter when crossing a function boundary:**
|
|
40
|
+
```vue
|
|
41
|
+
<script setup lang="ts">
|
|
42
|
+
import { watch, computed } from 'vue'
|
|
43
|
+
import { useFetch } from './useFetch'
|
|
44
|
+
|
|
45
|
+
// defaults via plain destructure syntax — replaces withDefaults()
|
|
46
|
+
const { id, count = 0 } = defineProps<{ id: number; count?: number }>()
|
|
47
|
+
|
|
48
|
+
// ✅ getter preserves reactivity
|
|
49
|
+
watch(() => id, () => reload())
|
|
50
|
+
const { data } = useFetch(() => id)
|
|
51
|
+
|
|
52
|
+
// ✅ bare access is fine inside computed / template / watchEffect
|
|
53
|
+
const doubled = computed(() => count * 2)
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<p>{{ count }} → {{ doubled }}</p>
|
|
58
|
+
</template>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Correct - composable consuming a getter:**
|
|
62
|
+
```ts
|
|
63
|
+
import { toValue, watchEffect, type MaybeRefOrGetter } from 'vue'
|
|
64
|
+
|
|
65
|
+
export function useFetch(id: MaybeRefOrGetter<number>) {
|
|
66
|
+
watchEffect(() => {
|
|
67
|
+
const current = toValue(id) // normalizes getter | ref | plain value
|
|
68
|
+
// fetch with `current`…
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Notes
|
|
74
|
+
|
|
75
|
+
- This is the recommended way to set prop defaults in 3.5+. If a component still uses `withDefaults()`, its caveats (e.g. mutable factory defaults) continue to apply — prefer migrating to destructure defaults for new code.
|
|
76
|
+
- The getter rule applies **only** across function boundaries. Bare `count` in a template or `computed` body is fully reactive; do not over-wrap.
|
|
77
|
+
|
|
78
|
+
## Reference
|
|
79
|
+
- [Vue.js — Reactive Props Destructure](https://vuejs.org/guide/components/props.html#reactive-props-destructure)
|
|
80
|
+
- [Vue 3.5 release notes](https://blog.vuejs.org/posts/vue-3-5)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Vapor Mode (Vue 3.6, Experimental)
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: Vapor is opt-in and unstable; adopting it app-wide or expecting full ecosystem support is premature
|
|
5
|
+
type: capability
|
|
6
|
+
tags: [vue3, vue36, vapor, performance, experimental, compiler]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vapor Mode (Vue 3.6, Experimental)
|
|
10
|
+
|
|
11
|
+
**Impact: LOW (forward-looking)** - Vapor Mode is an alternative compilation strategy in Vue 3.6 that drops the Virtual DOM and compiles components to direct, fine-grained DOM updates (Solid-like), for smaller bundles and less memory. As of the Vue 3.6 beta it is **feature-complete but unstable** — opt in per component, do not bet a production app on it, and do not present it as the default.
|
|
12
|
+
|
|
13
|
+
This is an experimental, version-gated feature. Treat everything below as subject to change.
|
|
14
|
+
|
|
15
|
+
## Task Checklist
|
|
16
|
+
|
|
17
|
+
- [ ] Do **not** enable Vapor by default; use the standard VDOM build unless a measured hotspot justifies it
|
|
18
|
+
- [ ] Opt in **per component** with `<script setup vapor>`
|
|
19
|
+
- [ ] Keep Vapor components Composition API + `<script setup>` only (no Options API)
|
|
20
|
+
- [ ] When mixing Vapor and VDOM components, register `vaporInteropPlugin`
|
|
21
|
+
- [ ] Confirm UI libraries work inside a Vapor region before relying on them
|
|
22
|
+
|
|
23
|
+
**Opt in per component:**
|
|
24
|
+
```vue
|
|
25
|
+
<script setup vapor>
|
|
26
|
+
// Composition API only; no Options API, getCurrentInstance() returns null here
|
|
27
|
+
import { ref } from 'vue'
|
|
28
|
+
const count = ref(0)
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<button @click="count++">{{ count }}</button>
|
|
33
|
+
</template>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Mixing with an existing VDOM app — interop plugin required:**
|
|
37
|
+
```ts
|
|
38
|
+
import { createApp, vaporInteropPlugin } from 'vue'
|
|
39
|
+
import App from './App.vue'
|
|
40
|
+
|
|
41
|
+
createApp(App).use(vaporInteropPlugin).mount('#app')
|
|
42
|
+
// Vapor and VDOM components can then nest in each other.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Fully-Vapor app (smallest baseline, no VDOM runtime):**
|
|
46
|
+
```ts
|
|
47
|
+
import { createVaporApp } from 'vue'
|
|
48
|
+
import App from './App.vue'
|
|
49
|
+
|
|
50
|
+
createVaporApp(App).mount('#app')
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Constraints & caveats (3.6 beta)
|
|
54
|
+
|
|
55
|
+
- `<script setup>` + Composition API only; Options API is unsupported.
|
|
56
|
+
- `getCurrentInstance()` returns `null` in Vapor components — libraries depending on it can break.
|
|
57
|
+
- Interop has rough edges (e.g. Vapor slots inside VDOM components need `renderSlot`); expect issues with some VDOM-based UI libraries.
|
|
58
|
+
- Intended adoption today: a performance-sensitive sub-region of an existing app, or a small greenfield app built fully in Vapor.
|
|
59
|
+
- `defineVaporComponent` exists but its API (TS generics, runtime props) is still evolving.
|
|
60
|
+
|
|
61
|
+
> Separately, Vue 3.6 rewrites `@vue/reactivity` on alien-signals, giving performance and memory gains to **all** 3.6 apps — independent of Vapor and with no API change.
|
|
62
|
+
|
|
63
|
+
## Reference
|
|
64
|
+
- [Vue.js Nation 2025 — Evan You on Vue 3.6 & Vapor Mode](https://vueschool.io/articles/news/vn-talk-evan-you-preview-of-vue-3-6-vapor-mode)
|
|
65
|
+
- [vuejs/core v3.6.0-beta release notes](https://github.com/vuejs/core/releases)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Vue 3.5 Helpers (useId, onWatcherCleanup, deferred Teleport, lazy hydration)
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: Reaching for manual id generation, ad-hoc watcher cleanup, or eager hydration when a built-in 3.5 helper exists
|
|
5
|
+
type: best-practice
|
|
6
|
+
tags: [vue3, vue35, useId, onWatcherCleanup, teleport, hydration, ssr, composition-api]
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vue 3.5 Helpers (useId, onWatcherCleanup, deferred Teleport, lazy hydration)
|
|
10
|
+
|
|
11
|
+
**Impact: MEDIUM** - Vue 3.5 (stable) ships small built-ins that replace hand-rolled patterns. Reach for these before writing your own.
|
|
12
|
+
|
|
13
|
+
## Task Checklist
|
|
14
|
+
|
|
15
|
+
- [ ] Use `useId()` for SSR-safe unique ids (form `:for`/`:id`, a11y attributes)
|
|
16
|
+
- [ ] Register watcher teardown with `onWatcherCleanup()` instead of tracking cleanup manually
|
|
17
|
+
- [ ] Use `<Teleport defer>` when the target element renders later in the same template
|
|
18
|
+
- [ ] Hydrate heavy SSR async components lazily with a `hydrate` strategy
|
|
19
|
+
|
|
20
|
+
### `useId()` — SSR-stable unique ids
|
|
21
|
+
|
|
22
|
+
Generates an id that matches across server and client render, avoiding hydration mismatches. Each call returns a distinct value.
|
|
23
|
+
|
|
24
|
+
```vue
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { useId } from 'vue'
|
|
27
|
+
const id = useId()
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<label :for="id">Name</label>
|
|
32
|
+
<input :id="id" />
|
|
33
|
+
</template>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Do not call `useId()` inside a `computed()`; declare it at setup top level.
|
|
37
|
+
|
|
38
|
+
### `onWatcherCleanup()` — cancel in-flight work
|
|
39
|
+
|
|
40
|
+
Registers cleanup that runs before the watcher re-fires or on unmount. Must be called **synchronously** within the effect (before any `await`).
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { watch, onWatcherCleanup } from 'vue'
|
|
44
|
+
|
|
45
|
+
watch(id, (newId) => {
|
|
46
|
+
const controller = new AbortController()
|
|
47
|
+
fetch(`/api/items/${newId}`, { signal: controller.signal })
|
|
48
|
+
onWatcherCleanup(() => controller.abort()) // abort the stale request
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `<Teleport defer>` — target rendered later
|
|
53
|
+
|
|
54
|
+
Without `defer`, `<Teleport>` needs its target to already exist. `defer` delays mounting until after the current render tick, so the target can appear later in the same template.
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<template>
|
|
58
|
+
<Teleport defer target="#modal-host">
|
|
59
|
+
<Modal />
|
|
60
|
+
</Teleport>
|
|
61
|
+
<!-- target defined after the Teleport in the same template -->
|
|
62
|
+
<div id="modal-host" />
|
|
63
|
+
</template>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Lazy hydration for async components (SSR)
|
|
67
|
+
|
|
68
|
+
Defer hydrating heavy, below-the-fold components until they are needed, cutting time-to-interactive. Strategies are imported from `vue`.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import {
|
|
72
|
+
defineAsyncComponent,
|
|
73
|
+
hydrateOnVisible,
|
|
74
|
+
hydrateOnIdle,
|
|
75
|
+
hydrateOnInteraction,
|
|
76
|
+
hydrateOnMediaQuery,
|
|
77
|
+
} from 'vue'
|
|
78
|
+
|
|
79
|
+
const HeavyChart = defineAsyncComponent({
|
|
80
|
+
loader: () => import('./HeavyChart.vue'),
|
|
81
|
+
hydrate: hydrateOnVisible({ rootMargin: '100px' }), // IntersectionObserver
|
|
82
|
+
// hydrateOnIdle(timeout?) → requestIdleCallback
|
|
83
|
+
// hydrateOnInteraction('click') → hydrates on first interaction
|
|
84
|
+
// hydrateOnMediaQuery('(min-width: 768px)')→ hydrates when the query matches
|
|
85
|
+
})
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Reference
|
|
89
|
+
- [Vue.js — Composition API helpers (useId, onWatcherCleanup)](https://vuejs.org/api/composition-api-helpers.html)
|
|
90
|
+
- [Vue.js — Teleport (defer)](https://vuejs.org/guide/built-ins/teleport.html)
|
|
91
|
+
- [Vue.js — Lazy hydration](https://vuejs.org/guide/components/async.html#lazy-hydration)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: create-adaptable-composable
|
|
3
|
-
description: Create a library-grade Vue composable that accepts maybe-reactive inputs (MaybeRef / MaybeRefOrGetter) so callers can pass a plain value, ref, or getter. Normalize inputs with toValue()/toRef() inside reactive effects (watch/watchEffect) to keep behavior predictable and reactive. Use this skill when user asks for creating adaptable or reusable composables.
|
|
4
|
-
license: MIT
|
|
5
|
-
metadata:
|
|
6
|
-
author: github.com/Pythoughts-labs
|
|
7
|
-
version: "17.0.0"
|
|
8
|
-
compatibility: Requires Vue 3 (or above) or Nuxt 3 (or above) project
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
# Create Adaptable Composable
|
|
12
|
-
|
|
13
|
-
Adaptable composables are reusable functions that can accept both reactive and non-reactive inputs. This allows developers to use the composable in a variety of contexts without worrying about the reactivity of the inputs.
|
|
14
|
-
|
|
15
|
-
Steps to design an adaptable composable in Vue.js:
|
|
16
|
-
1. Confirm the composable's purpose and API design and expected inputs/outputs.
|
|
17
|
-
2. Identify inputs params that should be reactive (MaybeRef / MaybeRefOrGetter).
|
|
18
|
-
3. Use `toValue()` or `toRef()` to normalize inputs inside reactive effects.
|
|
19
|
-
4. Implement the core logic of the composable using Vue's reactivity APIs.
|
|
20
|
-
|
|
21
|
-
## Core Type Concepts
|
|
22
|
-
|
|
23
|
-
### Type Utilities
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
/**
|
|
27
|
-
* value or writable ref (value/ref/shallowRef/writable computed)
|
|
28
|
-
*/
|
|
29
|
-
export type MaybeRef<T = any> = T | Ref<T> | ShallowRef<T> | WritableComputedRef<T>;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* MaybeRef<T> + ComputedRef<T> + () => T
|
|
33
|
-
*/
|
|
34
|
-
export type MaybeRefOrGetter<T = any> = MaybeRef<T> | ComputedRef<T> | (() => T);
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Policy and Rules
|
|
38
|
-
|
|
39
|
-
- Read-only, computed-friendly input: use `MaybeRefOrGetter`
|
|
40
|
-
- Needs to be writable / two-way input: use `MaybeRef`
|
|
41
|
-
- Parameter might be a function value (callback/predicate/comparator): do not use `MaybeRefOrGetter`, or you may accidentally invoke it as a getter.
|
|
42
|
-
- DOM/Element targets: if you want computed/derived targets, use `MaybeRefOrGetter`.
|
|
43
|
-
|
|
44
|
-
When `MaybeRefOrGetter` or `MaybeRef` is used:
|
|
45
|
-
- resolve reactive value using `toRef()` (e.g. watcher source)
|
|
46
|
-
- resolve non-reactive value using `toValue()`
|
|
47
|
-
|
|
48
|
-
### Examples
|
|
49
|
-
|
|
50
|
-
Adaptable `useDocumentTitle` Composable: read-only title parameter
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
|
-
import { watch, toRef } from 'vue'
|
|
54
|
-
import type { MaybeRefOrGetter } from 'vue'
|
|
55
|
-
|
|
56
|
-
export function useDocumentTitle(title: MaybeRefOrGetter<string>) {
|
|
57
|
-
watch(toRef(title), (t) => {
|
|
58
|
-
document.title = t
|
|
59
|
-
}, { immediate: true })
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Adaptable `useCounter` Composable: two-way writable count parameter
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
import { watch, toRef } from 'vue'
|
|
67
|
-
import type { MaybeRef } from 'vue'
|
|
68
|
-
|
|
69
|
-
function useCounter(count: MaybeRef<number>) {
|
|
70
|
-
const countRef = toRef(count)
|
|
71
|
-
function add() {
|
|
72
|
-
countRef.value++
|
|
73
|
-
}
|
|
74
|
-
return { add }
|
|
75
|
-
}
|
|
76
|
-
```
|