Vue.js has rapidly become one of the most popular JavaScript frameworks for building dynamic web applications, thanks to its simplicity, flexibility, and gentle learning curve. Unlike monolithic frameworks that require you to adopt an entire ecosystem, Vue is progressive—meaning you can start small and scale up as needed. Whether you’re a complete beginner or transitioning from other frameworks like React or Angular, Vue.js makes it easy to build interactive, modern web apps.
In this tutorial, we’ll walk through the fundamentals of Vue.js, from setting up your environment to building a fully functional dynamic app. By the end, you’ll have the skills to create your own Vue-powered projects and understand core concepts like reactivity, components, routing, and state management.
Table of Contents
- Introduction to Vue.js
- Setting Up Your Development Environment
- Vue.js Fundamentals
- Component-Based Architecture
- Building a Simple Dynamic App: Todo List
- Client-Side Routing with Vue Router
- State Management with Pinia
- Deploying Your Vue App
- Conclusion
- References
Setting Up Your Development Environment
Before diving into Vue, you’ll need to set up your development environment. Here’s what you need:
Prerequisites
- Basic knowledge of HTML, CSS, and JavaScript.
- Node.js (v14.0.0+ recommended) and npm (Node Package Manager), which comes with Node.js.
To check if Node.js is installed, run:
node -v
npm -v
If not installed, download Node.js from nodejs.org.
Installing Vue
Vue offers two main tools for project setup:
1. Vite (Recommended for New Projects)
Vite is a fast build tool that replaces the older Vue CLI. It’s optimized for speed and modern workflows.
To create a new Vue project with Vite:
npm create vite@latest my-vue-app -- --template vue
- Follow the prompts: Enter your project name (e.g.,
my-vue-app), select “Vue” as the framework, and “JavaScript” as the variant. - Navigate to the project folder and install dependencies:
cd my-vue-app npm install - Start the development server:
npm run dev
Your app will run at http://localhost:5173. Open this in your browser to see a default Vue welcome page.
2. Vue CLI (Legacy, Still Supported)
If you prefer the older CLI (not recommended for new projects), install it globally first:
npm install -g @vue/cli
Then create a project:
vue create my-vue-app
Vue.js Fundamentals
Let’s start with the core concepts of Vue.js.
The Vue Instance
At the heart of every Vue app is a Vue instance—an object that connects your data to the DOM. In a Vite project, this is managed automatically in src/main.js, but let’s simplify to understand the basics.
A basic Vue instance looks like this:
const app = Vue.createApp({
data() {
return {
message: "Hello, Vue!"
};
}
});
app.mount("#app"); // Mounts the app to the DOM element with id="app"
data(): Returns an object of reactive data properties. When these properties change, the DOM updates automatically.mount("#app"): Attaches the Vue instance to the HTML element withid="app".
Templates and Directives
Vue uses templates (HTML with special syntax) to define the UI. Templates can include directives—special attributes prefixed with v- that add reactivity to the DOM.
Common Directives:
-
v-text: Sets the text content of an element.<p v-text="message"></p> <!-- Equivalent to <p>{{ message }}</p> --> -
v-bind: Binds an HTML attribute to a data property (shorthand::).<img :src="imageUrl" :alt="message"> -
v-on: Listens to DOM events (shorthand:@).<button @click="count++">Click me</button> -
v-model: Creates two-way data binding for form inputs (syncs input value with data).<input v-model="username" placeholder="Enter name"> <p>Hello, {{ username }}!</p> -
v-for: Renders a list based on an array.<ul> <li v-for="item in items" :key="item.id">{{ item.name }}</li> </ul> -
v-if/v-else: Conditionally renders elements.<p v-if="isLoggedIn">Welcome back!</p> <p v-else>Please log in.</p>
Reactivity: How Vue Updates the DOM
Vue’s reactivity system tracks changes to data properties and updates the DOM automatically. Here’s how it works:
- When you create a Vue instance, Vue converts all properties in
data()into reactive getters/setters. - When a component renders, Vue tracks which data properties are used (dependency tracking).
- When a reactive property changes, Vue re-renders the components that depend on it (re-rendering).
Example: A simple counter with reactivity:
const app = Vue.createApp({
data() {
return { count: 0 };
},
});
app.mount("#app");
<div id="app">
<p>Count: {{ count }}</p>
<button @click="count++">Increment</button>
</div>
Clicking the button updates count, and Vue automatically updates the <p> tag.
Component-Based Architecture
Vue apps are built with components—reusable, self-contained pieces of UI. Components make code modular, easier to test, and scalable.
Creating Your First Component
Components can be global (available everywhere) or local (only in the parent component that defines them).
Global Component Example:
In src/main.js, register a global component:
import { createApp } from 'vue';
import App from './App.vue';
import HelloWorld from './components/HelloWorld.vue';
const app = createApp(App);
app.component('hello-world', HelloWorld); // Global component
app.mount('#app');
Now use it in any template:
<hello-world></hello-world>
Local Component Example:
Define a component locally in another component (e.g., App.vue):
<template>
<div>
<local-component></local-component>
</div>
</template>
<script>
import LocalComponent from './components/LocalComponent.vue';
export default {
components: {
LocalComponent // Local to App.vue
}
};
</script>
Props: Passing Data to Components
Props allow you to pass data from a parent component to a child component. They are defined in the child component and passed via attributes in the parent.
Child Component (TodoItem.vue):
<template>
<li>{{ todo.text }}</li>
</template>
<script>
export default {
props: {
todo: {
type: Object, // Validate prop type
required: true // Mark as required
}
}
};
</script>
Parent Component:
<template>
<ul>
<todo-item :todo="item" v-for="item in todos" :key="item.id"></todo-item>
</ul>
</template>
<script>
import TodoItem from './TodoItem.vue';
export default {
components: { TodoItem },
data() {
return {
todos: [
{ id: 1, text: "Learn Vue" },
{ id: 2, text: "Build an app" }
]
};
}
};
</script>
Events: Communicating Child to Parent
To send data from a child to a parent, use custom events. The child emits an event with $emit, and the parent listens with @event-name.
Child Component (TodoItem.vue):
<template>
<li>
{{ todo.text }}
<button @click="deleteTodo">×</button>
</li>
</template>
<script>
export default {
props: { todo: { type: Object, required: true } },
methods: {
deleteTodo() {
this.$emit('delete-todo', this.todo.id); // Emit event with todo ID
}
}
};
</script>
Parent Component:
<template>
<ul>
<todo-item
:todo="item"
v-for="item in todos"
:key="item.id"
@delete-todo="removeTodo"
></todo-item>
</ul>
</template>
<script>
export default {
methods: {
removeTodo(todoId) {
this.todos = this.todos.filter(todo => todo.id !== todoId);
}
}
};
</script>
Building a Simple Dynamic App: Todo List
Let’s apply what we’ve learned by building a todo list app with:
- A form to add new todos.
- A list of todos with delete buttons.
- Component-based structure.
Project Setup
If you haven’t already, create a new Vite project:
npm create vite@latest todo-app -- --template vue
cd todo-app
npm install
npm run dev
Creating the Todo Components
1. TodoInput.vue (Child Component):
Handles the input form to add new todos.
<template>
<form @submit.prevent="addTodo">
<input
v-model="newTodoText"
placeholder="Add a new todo..."
required
>
<button type="submit">Add</button>
</form>
</template>
<script>
export default {
data() {
return { newTodoText: "" };
},
methods: {
addTodo() {
this.$emit("add-todo", this.newTodoText); // Emit new todo text
this.newTodoText = ""; // Clear input
}
}
};
</script>
2. TodoList.vue (Parent Component):
Renders the list of todos and coordinates with TodoInput and TodoItem.
<template>
<div class="todo-list">
<h1>Todo List</h1>
<TodoInput @add-todo="handleAddTodo" />
<ul>
<TodoItem
v-for="todo in todos"
:key="todo.id"
:todo="todo"
@delete-todo="handleDeleteTodo"
/>
</ul>
</div>
</template>
<script>
import TodoInput from './TodoInput.vue';
import TodoItem from './TodoItem.vue';
export default {
components: { TodoInput, TodoItem },
data() {
return {
todos: [
{ id: 1, text: "Learn Vue components" },
{ id: 2, text: "Build a todo app" }
],
nextId: 3
};
},
methods: {
handleAddTodo(text) {
this.todos.push({ id: this.nextId++, text });
},
handleDeleteTodo(todoId) {
this.todos = this.todos.filter(todo => todo.id !== todoId);
}
}
};
</script>
3. TodoItem.vue (Child Component):
Renders a single todo item with a delete button.
<template>
<li>
{{ todo.text }}
<button @click="deleteTodo">×</button>
</li>
</template>
<script>
export default {
props: {
todo: { type: Object, required: true }
},
methods: {
deleteTodo() {
this.$emit('delete-todo', this.todo.id);
}
}
};
</script>
<style scoped>
li {
display: flex;
justify-content: space-between;
margin: 0.5rem 0;
padding: 0.5rem;
border: 1px solid #ddd;
}
button {
background: #ff4444;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
}
</style>
4. Update App.vue:
Import TodoList and use it as the main component.
<template>
<div id="app">
<TodoList />
</div>
</template>
<script>
import TodoList from './components/TodoList.vue';
export default {
components: { TodoList }
};
</script>
<style>
#app {
max-width: 600px;
margin: 2rem auto;
padding: 0 1rem;
font-family: Arial, sans-serif;
}
</style>
Adding Functionality: Add/Delete Todos
Test your app by adding todos and clicking the delete button. The TodoInput emits an add-todo event with the text, and TodoList adds it to the todos array. TodoItem emits delete-todo when the button is clicked, and TodoList filters it out.
Client-Side Routing with Vue Router
Most dynamic apps need multiple pages (e.g., home, about, settings). Vue Router handles client-side routing, enabling navigation without reloading the page.
Installing Vue Router
In your todo-app project, install Vue Router:
npm install vue-router@4
Setting Up Routes
1. Create a router folder and index.js:
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../views/Home.vue';
import About from '../views/About.vue';
const routes = [
{ path: '/', name: 'Home', component: Home },
{ path: '/about', name: 'About', component: About }
];
const router = createRouter({
history: createWebHistory(), // Uses HTML5 history mode (no hash in URL)
routes
});
export default router;
2. Create views Folder:
-
src/views/Home.vue: Contains theTodoListcomponent.<template> <TodoList /> </template> <script> import TodoList from '../components/TodoList.vue'; export default { components: { TodoList } }; </script> -
src/views/About.vue: A simple about page.<template> <div class="about"> <h1>About This App</h1> <p>A simple todo app built with Vue.js!</p> </div> </template>
3. Update main.js to Use Router:
// src/main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App)
.use(router) // Use the router
.mount('#app');
Navigating Between Pages
Update App.vue to include navigation links and a router view:
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view /> <!-- Renders the current route component -->
</div>
</template>
<style>
/* Add styling for navigation links */
nav {
margin: 2rem 0;
}
router-link {
margin-right: 1rem;
text-decoration: none;
color: #42b983; /* Vue's brand color */
}
router-link.router-link-exact-active {
font-weight: bold;
text-decoration: underline;
}
</style>
Now you can navigate between the Home (todo list) and About pages!
State Management with Pinia
As your app grows, you may need to share state across components (e.g., user auth, global settings). Pinia is Vue’s official state management library (replaces Vuex) and simplifies managing shared state.
What is Pinia?
Pinia provides a centralized store for reactive state that can be accessed by any component. Key features:
- Simple API with no nested modules.
- TypeScript support.
- DevTools integration.
Creating a Pinia Store for Todos
Let’s refactor our todo app to use Pinia, so the todos state is shared across components.
1. Install Pinia:
npm install pinia
2. Update main.js to Use Pinia:
// src/main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia'; // Import Pinia
import App from './App.vue';
import router from './router';
const app = createApp(App);
app.use(createPinia()); // Use Pinia
app.use(router);
app.mount('#app');
3. Create a stores Folder and todoStore.js:
// src/stores/todoStore.js
import { defineStore } from 'pinia';
// Define a store with the id 'todo'
export const useTodoStore = defineStore('todo', {
state: () => ({
todos: [
{ id: 1, text: "Learn Pinia" },
{ id: 2, text: "Refactor todo app" }
],
nextId: 3
}),
actions: {
addTodo(text) {
this.todos.push({ id: this.nextId++, text });
},
deleteTodo(todoId) {
this.todos = this.todos.filter(todo => todo.id !== todoId);
}
}
});
4. Update TodoList.vue to Use the Store:
Replace local todos state with the Pinia store.
<template>
<div class="todo-list">
<h1>Todo List</h1>
<TodoInput @add-todo="store.addTodo" />
<ul>
<TodoItem
v-for="todo in store.todos"
:key="todo.id"
:todo="todo"
@delete-todo="store.deleteTodo"
/>
</ul>
</div>
</template>
<script>
import { useTodoStore } from '../stores/todoStore';
import TodoInput from './TodoInput.vue';
import TodoItem from './TodoItem.vue';
export default {
components: { TodoInput, TodoItem },
setup() {
const store = useTodoStore(); // Access the store
return { store };
}
};
</script>
Now the todos state is managed globally, making it easy to access from other components (e.g., the About page could display the todo count).
Deploying Your Vue App
To share your app with the world, build a production-ready version and deploy it.
Build the Project
Run the build command to generate optimized assets in the dist folder:
npm run build
Deployment Options
Popular free options:
- Netlify: Drag-and-drop the
distfolder or connect to GitHub for auto-deployment. - Vercel: Similar to Netlify, with seamless GitHub integration.
- GitHub Pages: Deploy to a GitHub repo’s
gh-pagesbranch (requires extra config for Vue Router).
For Netlify/Vercel:
- Push your code to GitHub.
- Connect your repo to Netlify/Vercel.
- Set the build command to
npm run buildand the publish directory todist.
Conclusion
You’ve now learned the basics of building dynamic web apps with Vue.js! We covered:
- Setting up a Vue project with Vite.
- Core concepts: reactivity, directives, and components.
- Building a todo app with component communication.
- Adding routing with Vue Router.
- Managing global state with Pinia.
Vue’s simplicity and flexibility make it a great choice for beginners and experts alike. To continue learning:
- Explore the official Vue.js documentation.
- Experiment with the Composition API (a more flexible alternative to the Options API used here).
- Learn about testing Vue apps with tools like Cypress or Vitest.
References
- Vue.js Official Documentation
- Vite Documentation
- Vue Router Documentation
- Pinia Documentation
- Vue School (Free Tutorials)
- Vue Community Forum
Happy coding! 🚀