ai vue tutorial 2026-08-30
write a tutorial of vue js
ai answer
Vue.js Tutorial (Vue 3)
A practical guide to Vue 3 using the current recommended stack: Vite, Composition API, and <script setup>. Official docs:
https://vuejs.org/
.
You should already know HTML, CSS, and basic JavaScript. A downloadable copy of this tutorial is at the end.
1. What Vue is
Vue is a progressive JavaScript framework for user interfaces. It adds two ideas on top of HTML/CSS/JS:
- Declarative rendering — you describe the UI from state; Vue updates the DOM when state changes.
- Components — reusable pieces that own their template, logic, and styles.
You can drop Vue into one page with a <script> tag, or use it as a full SPA with a build tool, router, and store.
Vue 3 has two API styles. Options API (data, methods, mounted) is fine for simple components. Composition API (ref, computed, onMounted) is the official choice for new apps, especially with TypeScript. This tutorial uses Composition API.
2. How to start
No build step (try it in a file)
<!DOCTYPE html> <html> <head> <title>Hello Vue</title> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> </head> <body> <div id="app"> <h1>{{ message }}</h1> <button @click="count++">Clicked {{ count }} times</button> </div> <script> const { createApp, ref } = Vue createApp({ setup() { const message = ref('Hello Vue') const count = ref(0) return { message, count } } }).mount('#app') </script> </body> </html>
{{ message }} interpolates text. @click listens for clicks. ref() is reactive state.
Real project (recommended)
npm create vue@latest cd your-project-name npm install npm run dev
Vite serves the app (usually http://localhost:5173). Use VS Code plus the Vue - Official (Volar) extension.
npm run dev # hot-reload dev server npm run build # production build → dist/ npm run preview # serve the production build
3. Single-File Components
A .vue file is one component:
<script setup> import { ref } from 'vue' const title = ref('My first component') </script> <template> <h1>{{ title }}</h1> </template> <style scoped> h1 { color: #42b883; } </style>
Bindings in <script setup> are available in the template automatically. scoped CSS applies only to that component.
main.js mounts the root:
import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
4. Reactivity
ref — any value
In script, use .value. In template, Vue unwraps it.
<script setup> import { ref } from 'vue' const count = ref(0) function increment() { count.value++ } </script> <template> <button @click="increment">{{ count }}</button> </template>
reactive — objects
<script setup> import { reactive } from 'vue' const user = reactive({ name: 'Ada', age: 36 }) </script> <template> <p>{{ user.name }} is {{ user.age }}</p> </template>
computed — derived, cached state
<script setup> import { ref, computed } from 'vue' const first = ref('Ada') const last = ref('Lovelace') const fullName = computed(() => `${first.value} ${last.value}`) </script>
Use computed when you derive a value. Use a method when you do something (click, submit, fetch).
5. Template syntax
<!-- bind attributes --> <img :src="imageUrl" :alt="name" /> <button :disabled="saving">Save</button> <!-- conditionals --> <p v-if="ok">All good</p> <p v-else>Error</p> <p v-show="open">Toggled with CSS</p> <!-- lists — always use a stable :key --> <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li> <!-- events --> <button @click="save">Save</button> <form @submit.prevent="onSubmit">…</form> <input @keyup.enter="submit" />
: is v-bind. @ is v-on.
v-if adds/removes DOM. v-show toggles display.
6. Forms: v-model
Two-way binding:
<script setup> import { ref } from 'vue' const text = ref('') const agreed = ref(false) </script> <template> <input v-model="text" placeholder="Name" /> <label><input type="checkbox" v-model="agreed" /> I agree</label> </template>
On a child component (Vue 3.4+):
<script setup> const model = defineModel() </script> <template> <input v-model="model" /> </template>
7. Components: props down, events up
Child (HelloName.vue)
<script setup> const props = defineProps({ name: { type: String, required: true } }) const emit = defineEmits(['greet']) function sayHi() { emit('greet', `Hello, ${props.name}`) } </script> <template> <button @click="sayHi">Greet {{ name }}</button> </template>
Parent
<script setup> import HelloName from './HelloName.vue' function onGreet(msg) { alert(msg) } </script> <template> <HelloName name="Ada" @greet="onGreet" /> </template>
Do not mutate props. Emit an event (or use defineModel).
Slots let the parent inject markup:
<!-- Card.vue --> <template> <section> <header><slot name="header" /></header> <slot /> </section> </template>
<Card> <template #header>Title</template> <p>Body</p> </Card>
8. Lifecycle and watchers
<script setup> import { onMounted, onUnmounted, watch, watchEffect } from 'vue' onMounted(() => { /* fetch, measure DOM, attach widgets */ }) onUnmounted(() => { /* clear timers and listeners */ }) watch(query, async (q) => { results.value = q ? await search(q) : [] }) watchEffect(() => { localStorage.setItem('query', query.value) }) </script>
computed→ derive a valuewatch/watchEffect→ run a side effect
9. Fetching data
<script setup> import { ref, onMounted } from 'vue' const posts = ref([]) const loading = ref(true) const error = ref(null) onMounted(async () => { try { const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5') if (!res.ok) throw new Error('Request failed') posts.value = await res.json() } catch (e) { error.value = e.message } finally { loading.value = false } }) </script> <template> <p v-if="loading">Loading…</p> <p v-else-if="error">{{ error }}</p> <ul v-else> <li v-for="post in posts" :key="post.id">{{ post.title }}</li> </ul> </template>
10. Composables (reusable logic)
// useCounter.js import { ref } from 'vue' export function useCounter(initial = 0) { const count = ref(initial) const increment = () => count.value++ return { count, increment } }
<script setup> import { useCounter } from './useCounter' const { count, increment } = useCounter(10) </script>
This is the main reason to use the Composition API: share logic without mixins.
11. Mini app: todos
<script setup> import { ref, computed } from 'vue' const newTodo = ref('') const todos = ref([ { id: 1, text: 'Read the Vue guide', done: true }, { id: 2, text: 'Build a todo app', done: false } ]) const remaining = computed(() => todos.value.filter(t => !t.done).length) function addTodo() { const text = newTodo.value.trim() if (!text) return todos.value.push({ id: Date.now(), text, done: false }) newTodo.value = '' } function removeTodo(id) { todos.value = todos.value.filter(t => t.id !== id) } </script> <template> <h1>Todos</h1> <p>{{ remaining }} left</p> <form @submit.prevent="addTodo"> <input v-model="newTodo" placeholder="What needs doing?" /> <button type="submit">Add</button> </form> <ul> <li v-for="todo in todos" :key="todo.id"> <label> <input type="checkbox" v-model="todo.done" /> <span :class="{ done: todo.done }">{{ todo.text }}</span> </label> <button @click="removeTodo(todo.id)">×</button> </li> </ul> </template>
That one file uses ref, computed, v-model, v-for, events, and form submit — the everyday toolkit.
12. Router and Pinia (when you need them)
Vue Router — pages without full reloads:
import { createRouter, createWebHistory } from 'vue-router' export const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', component: () => import('./pages/Home.vue') }, { path: '/about', component: () => import('./pages/About.vue') } ] })
createApp(App).use(router).mount('#app')
<template> <RouterLink to="/">Home</RouterLink> <RouterView /> </template>
Pinia — shared state across distant components:
import { defineStore } from 'pinia' import { ref, computed } from 'vue' export const useCounterStore = defineStore('counter', () => { const count = ref(0) const doubled = computed(() => count.value * 2) function increment() { count.value++ } return { count, doubled, increment } })
Keep local UI state in the component. Use a store when several screens need the same data. Pinia replaced Vuex as the official recommendation.
13. Common mistakes
- Forgetting
.valuein script (count++on a ref does nothing useful). - Mutating props instead of emitting an event.
- Missing a stable
:keyonv-for. - Using a method where
computedbelongs. - Replacing a
reactiveobject (state = { ... }drops the proxy). Mutate fields, or use aref.
14. What to do next
- Rebuild the todo app from memory.
- Add All / Active / Completed filters.
- Persist todos with
watch+localStorage. - Split each item into a child component.
- Add Vue Router and a Pinia store.
| Resource | Link |
|---|---|
| Interactive official tutorial | https://vuejs.org/tutorial/ |
| Full guide | vuejs.org/guide |
| Router | router.vuejs.org |
| Pinia | pinia.vuejs.org |
| Full-stack / SSR | Nuxt |
Vue stays small on purpose: templates, reactivity, components. Router, store, and meta-frameworks are opt-in.
Full markdown version (same content, easier to save or print):
If you want a follow-up, I can turn this into a step-by-step project (todo app with router + Pinia), a TypeScript version, or an Options API companion.