Learning Logs · · 3 min read
Learning Vue 3: installation to component patterns
Vue 3 from scratch: scaffolding with create-vue, Single File Component anatomy, ref/computed, v-model, and the golden props-down-events-up rule — with a simple shop example.
Vue 3 is the friendliest framework for moving from plain HTML/JS to reactive apps — its templates still feel like HTML. This tutorial goes from installation to the component patterns I use on real projects.
1. Installation
Prerequisite: Node.js 20+. Scaffold the official project (Vite under the hood):
npm create vue@latest shop-app
# answer the prompts (TypeScript? Router? Pinia?) — while learning, answer No to everything
cd shop-app
npm install
npm run dev # open http://localhost:5173
2. Anatomy of a Single File Component (SFC)
One .vue file = logic + template + style in one place:
<script setup>
import { ref, computed } from "vue";
const cart = ref([]); // reactive state
const total = computed(() => // derived value — recomputes automatically
cart.value.reduce((s, item) => s + item.price, 0)
);
function add(item) {
cart.value.push(item);
}
</script>
<template>
<button @click="add({ name: 'Coffee', price: 25000 })">Add Coffee</button>
<p>{{ cart.length }} items — total {{ total.toLocaleString() }}</p>
</template>
Three things to memorize from this example:
ref()wraps a value to make it reactive — in<script>you accesscart.value, in the template justcart.computed()for derived values — never storetotalas separate state (the same anti-pattern as in React).@clickis shorthand forv-on:click.
3. Two-way forms with v-model
<script setup>
import { ref } from "vue";
const name = ref("");
</script>
<template>
<input v-model="name" placeholder="Product name" />
<p>Preview: {{ name || "(empty)" }}</p>
</template>
v-model = two-way binding — typing in the input changes name directly, and vice versa. This is why forms in Vue feel so much leaner than onChange + setState.
4. Components: props down, events up
<!-- ProductCard.vue -->
<script setup>
defineProps({ name: String, price: Number });
const emit = defineEmits(["buy"]);
</script>
<template>
<article class="card">
<h3>{{ name }}</h3>
<p>{{ price.toLocaleString() }}</p>
<button @click="emit('buy')">Buy</button>
</article>
</template>
<!-- Used in the parent -->
<ProductCard v-for="p in products" :key="p.id" :name="p.name" :price="p.price" @buy="add(p)" />
The golden rule is the same in every framework: data flows down through props, changes flow up through events. Never mutate props from a child.
Tips from experience
- Always give
v-fora unique:key— without it Vue recycles DOM in ways that cause subtle bugs. - Start without Pinia/Router; add them when you genuinely need them. Many small apps are fine with
ref+ props. <script setup>is the modern way — if another tutorial usesexport default { data() ... }, that’s the older Options API; both work, but learn the Composition API first.
Want something like this built for your business?