VueJS Workshop Notes
A practical guide to building interactive web applications with Vue 3, Vite, and modern tooling.
🚀 What is VueJS?
VueJS is a JavaScript framework for building Single Page Applications for the web. It uses HTML, CSS, JS and employs both component-based and declarative programmingmodels to build user interfaces.
It's a progressive framework — flexible and incrementally adoptable.
VueJS Use Cases:
- • Enhancing static HTML without a build step
- • Embedding as Web Components on any page
- • Single-Page Applications (SPA)
- • Fullstack / Server-Side Rendering (SSR)
- • Jamstack / Static Site Generation (SSG)
- • Targeting desktop, mobile, WebGL, and even the terminal
Before we begin: Install NodeJS — this is required to install JS libraries, frameworks, and packages via NPM throughout this workshop.
Understanding the Shift: Documents vs. Applications
Before we write our first line of code, it is essential to understand that we are not just learning a new tool; we are adopting a different mindset for how a website works. In the traditional approach, we build documents. In the Vue.js approach, we build an application.
Traditional Website (MPA)
Think of a basic HTML website as a physical filing cabinet. Each URL is like pulling out a complete, finished sheet of paper — every navigation is a fresh document.
Vue.js App (SPA)
One persistent application surface. Vue manages a single HTML shell (index.html) and uses JavaScript to swap content inside it — no full reloads.
The Concept of 'Components'
The secret to the SPA model is Components. In Vue, we break everything down into independent, interlocking blocks — like puzzle pieces that snap together to form a complete UI.
Everything is a Component
Key insight: When navigating from 'Home' to 'About', Vue keeps the Header and Footer mounted and simply swaps the main content component. This uses far less data and creates a smooth, fluid experience that feels like a native mobile app rather than a clunky sequence of page loads.
Setting up with Vite
Vite (French: "veet") is a local development server created by Evan You, the creator of Vue.js. It supports TypeScript, JSX, and uses Rollup/esbuild for bundling.
Key features: Hot Module Replacement (HMR), built-in SSR support, listens on port 5173 by default.
# Create Vue project with Vite
$ npm create vite@latest my-vue-app
# Select: Vue → JavaScript
# Install dependencies
$ cd my-vue-app && npm install
# Start dev server
$ npm run dev
# → http://localhost:5173After setup, you'll find a src folder with App.vue — your entry point.
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<HelloWorld msg="Vite + Vue" />
</div>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
</style>⚡ Understanding Reactivity
In Vue, reactive variables automatically update the UI when their values change.
<script setup>
import { ref } from 'vue'
defineProps({ msg: String })
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<button @click="count++">count is {{ count }}</button>
</template>Computed double: 0
Mustache syntax: Use {{ variable }}in templates to display reactive data. Vue handles the DOM updates automatically — no need forgetElementById().
// Plain JavaScript (tedious)
let count = 0
const el = document.getElementById('count')
function increment() {
count++
el.innerHTML = count // Manual DOM update
}
// Vue (declarative)
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>🧩 Component-Based Development
Vue uses Single-File Components (.vue) that combine template, script, and styles in one file.
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">
count is {{ count }}
</button>
</template>
<style scoped>
button {
background-color: #4CAF50;
color: white;
padding: 15px 32px;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>v-model — Two-Way Binding Demo
Hello, stranger! 👋
Scoped CSS: The <style scoped> attribute ensures styles only apply to the current component, preventing global CSS conflicts.
✅ Building a Todo List
<script setup>
import { ref } from 'vue'
const todos = ref([])
const todo = ref("")
function addTodo() {
todos.value.push(todo.value)
todo.value = ""
}
</script>
<template>
<input v-model="todo" placeholder="Add todo..." />
<button @click="addTodo">Add</button>
<ul>
<li v-for="t in todos" :key="t">{{ t }}</li>
</ul>
</template>No todos yet — add one above! ✨
v-model
Binds input value to component state. Changes in input update state; state changes update input.
v-for
Renders a list by looping over an array. Always use :key for performance.
📦 Props — Component Communication
Props allow parent components to pass data down to child components.
<script setup>
// Parent: App.vue
import TodoItem from './components/TodoItem.vue'
const todos = ref(['Learn Vue', 'Build app'])
</script>
<template>
<TodoItem v-for="todo in todos" :key="todo" :todo="todo" />
</template>
<!-- Child: TodoItem.vue -->
<script setup>
defineProps({ todo: String })
</script>
<template>
<li>{{ todo }}</li>
</template>Prop Definition Patterns
// Single prop
defineProps({ msg: String })
// Multiple props with types
defineProps({
msg: String,
count: Number,
isActive: Boolean,
items: Array,
config: Object
})
// With default values & validation
defineProps({
msg: { type: String, required: true },
count: { type: Number, default: 0 }
})Tip: Use :propName (shorthand for v-bind:propName) to pass dynamic values. Use plain propName="value" for static strings.
🎨 Styling with Tailwind CSS
Tailwind CSS is a utility-first framework — compose designs using small, pre-built classes directly in your markup.
<template>
<button
@click="count++"
class="bg-blue-500 hover:bg-blue-700 text-white
font-bold py-2 px-4 rounded transition"
>
count is {{ count }}
</button>
</template>Live Preview
Pro Tip: Install the Tailwind CSS IntelliSense VSCode extension for autocomplete, syntax highlighting, and linting. Learn more at tailwindcss.com.
🔗 Client-Side Routing with Vue Router
In SPAs, routing happens on the client — the browser doesn't reload. Vue Router maps URLs to components and handles navigation without full page refreshes.
// main.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from './components/Home.vue'
import About from './components/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
createApp(App).use(router).mount('#app')// Global guard
router.beforeEach((to, from, next) => {
const isLoggedIn = !!localStorage.getItem('token')
if (to.meta.requiresAuth && !isLoggedIn) {
next('/login')
} else {
next()
}
})<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
console.log(route.params.id)
const goHome = () => router.push('/')
</script>
<template>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterView />
</template>Router Components
<RouterView />— Renders the matched component for the current route<RouterLink to="/path">— Declarative navigation (replaces <a> tags)useRouter()/useRoute()— Composition API hooks for programmatic navigation
🌐 Making API Requests with Fetch
The Fetch API is built into modern browsers — no library needed. It returns Promises, making it perfect for async/await patterns.
<script setup>
import { ref } from 'vue'
const users = ref([])
fetch('https://api.github.com/users')
.then(res => res.json())
.then(data => { users.value = data })
.catch(err => console.error(err))
</script>
<template>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.login }}
</li>
</ul>
</template><script setup>
import { ref } from 'vue'
const username = ref('')
async function addUser() {
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username.value })
})
const json = await res.json()
// Handle response...
}
</script>
<template>
<input v-model="username" />
<button @click="addUser">Add User</button>
</template>Best Practice: Always handle loading and error states when fetching data. Use try/catch with async/await for cleaner error handling than .catch() chains.