A bunch of (scattered) tips and resources as I experiment with Vue.
- Basics:
General wisdomAnatomyEventhandlingWatchersComputed props - Components:
ComponentsPropsLifecycle hooksEmitting eventsSlots - Fetching Data:
Calling APIs in hooksUnique identifiers - Styling Components:
Global vs scoped stylesCSS modulesCSS v-bind - Composition API:
Composition APIReactive referencesscript setupComposables - Routing and Deployment:
Vue RouterHistoryDynamic routesDeployment - Advanced:
Pre-processorsPinia State Management
Overview
What is Vue?
an open-source model–view–viewmodel front end JavaScript framework for building user interfaces and single-page applications, created by Evan You
Helpful resouces:
- Read the offical docs
- Examples
- Vue cheat sheet
- Awesome Vue
- Vue.js devtools
- Volar VSCode extension
- Built-in Directives
General wisdom
- It’s best to stick to conventions of the web and use
camelCasein your script andkebab-casein your template - Don’t pass functions as
props, insteademitevents propscouples components to each other, for broad or deep cross cutting state, level up to state management- Test data sources: JSON Placeholder PokeAPI
Anatomy
Here is a bare bones vue app. There are literally 3 blocks for script, template (markup) and style:
<div id="app">
<p v-if="message.length % 2 === 0">Even: {{ message.toUpperCase() }}</p>
<p v-else>Odd: {{ message }}</p>
<ul v-for="item in listOfNumbers">
<li>
{{ item.id }}
<ul>
<li v-for="number in item.list">{{ number }}</li>
</ul>
</li>
</ul>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js" />
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
message: "Hello it works",
listOfNumbers: [
{
name: 1,
id: "6a887cd2-f0bf-4321-b192-92016f82a883",
list: [1, 2, 3],
},
{
name: 2,
id: "8d14d90b-2d47-473e-8293-d5c324111d0d",
list: [1, 2, 3],
},
],
};
},
});
app.mount("#app");
</script>
<style>
main {
display: flex;
justify-content: center;
flex-direction: column;
max-width: 320px;
margin: 0 auto;
}
main h1 {
margin-top: 10vh;
margin-bottom: 20px;
}
</style>
Notes
- The CDN include is all that is needed. No complex build toolchain (although that’s a well supported option)
- Templates use the mustache syntax
{{ }} - Directives are prefixed with
v-to indicate that they are special attributes provided by Vue, they apply special reactive behavior to the rendered DOM, keeping the HTML in-sync with the data that it’s bound to - The
v-ifdirective will destroy elements from the DOM as the condition is toggled (potentially performance expensive depending on the scenario), if desired thev-showdirective will preserve DOM but visually toggle using CSS
Cleaner data arrow syntax
// traditional syntax
const app = createApp({
data() {
return {
message: "Hello it works",
};
},
});
// arrow operator
const app = createApp({
data: () => ({
message: "Hello it works",
}),
});
Vue component conceptual model (the forest view):

Events
Reactive event listening (write) is done with the v-on directive, or the @ shorthand: v-on:click="handler" is the same as @click="handler"
The inverse direction (read) v-bind directive can similarly be used, with the colon : shorthand <HaltCatchStatistics :characters="characterList">
Vue has the notion of methods, which are cleverly component scoped, including the notorious this value which is rewired to only refer to the component instance.
Gotcha: Avoid arrow functions for methods, as it prevents Vue from binding the appropriate this
<button v-on:click="incrementCount">Increment</button>
<div>
<label for="incrementAmount">Icrement by:</label>
<input
type="number"
v-bind:value="incrementAmount"
v-on:input="changeIncrementAmount"
/>
</div>
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
count: 10,
incrementAmount: 8,
};
},
methods: {
incrementCount() {
this.count += this.incrementAmount;
},
},
});
</script>
Notice above how the read (v-bind) and write (v-on:input) are being handled. But isn’t there a reactive two way binding?
Enter v-model:
<input type="number" v-model="incrementAmount" />
v-model will also intelligently type (in the above case, numeric) the variable, so it won’t treat an int as a string.
Watchers
Handy in cases where we need to perform “side effects” in reaction to state changes - for example, changing another piece of state based on the result of an async operation.
With the Options API, we can use the watch option to trigger a function whenever a reactive property changes:
Use with care, as watchers can trigger a cascade of re-rendering work.
export default {
data() {
return {
question: "",
answer: "Questions usually contain a question mark. ;-)",
};
},
watch: {
// whenever question changes, this function will run
question(newQuestion, oldQuestion) {
if (newQuestion.includes("?")) {
this.getAnswer();
}
},
},
methods: {
async getAnswer() {
this.answer = "Thinking...";
try {
const res = await fetch("https://yesno.wtf/api");
this.answer = (await res.json()).answer;
} catch (error) {
this.answer = "Error! Could not reach the API. " + error;
}
},
},
};
Computed
Computed properties are a handy slice of vue magic. Basically think of them as a function, that will only be exercised if the underlying data on which it is based changes. In others words a function that automagically caches.
export default {
data() {
return {
author: {
name: "John Doe",
books: [
"Vue 2 - Advanced Guide",
"Vue 3 - Basic Guide",
"Vue 4 - The Mystery",
],
},
};
},
computed: {
// a computed getter
publishedBooksMessage() {
// `this` points to the component instance
return this.author.books.length > 0 ? "Yes" : "No";
},
},
};
Components
Involves creating vue files that follow a standard blueprint, usually with <script>, <template> and <style> blocks. This is known as a Vue SFC (Single File Component):
<script>
export default {
data: () => ({
count: 10,
incrementAmount: 8,
}),
methods: {
incrementCount() {
this.count += this.incrementAmount;
},
},
};
</script>
<template>
<h1>Counter</h1>
<p>{{ count }}</p>
<button v-on:click="incrementCount">Increment Count</button>
<h1>{{ incrementAmount }}</h1>
<div>
<label for="incrementAmount">Increment by:</label>
<input type="number" v-model="incrementAmount" />
</div>
</template>
Consuming the component involves importing and registering it using the Options API:
<script setup lang="js">
import Counter from './components/Counter.vue';
export default {
components: {
Counter
}
}
</script>
Then actually using it within the template:
<main>
<Counter />
</main>
Component tips:
- Get into habbit of using multi-word components, as the base HTML spec can shift.
- Vue supports kebab or pascal case out of the box e.g.
<FooCounter />or<foo-counter />both work well.
Props
Props are an explicit way for components to define their API to the outside world.
They are self documenting in that they not only define the name of the props, but can also specify their type and if they are mandatory or optional.
Remember props are intended for reads only, and NEVER for mutation.
export default {
props: {
title: String,
likes: Number,
description: {
type: String,
default: "A silly default string",
},
characters: {
type: Array,
required: true,
},
user: {
type: Object,
required: true,
},
},
};
Types are stanard JS types, such as Function, Object, String, Number and so on.
Conversly you can just be lazy with your props:
export default {
props: ["characters"],
};
Providing the props should follow kebab case (although camelCase works just fine) to align with how HTML attributes are defined and used:
<MyComponent greeting-message="hello" />
Remember for reactive data binary we must v-bind to it:
<UserCard v-bind:user="userData" />
Or more consisely:
<UserCard :user="userData" />
Lifecycle hooks
Lifecycle hooks provide an opportunity to run custom logic during the many phases a component goes through:
These can simply be registered right inside the Options API hunk within the component, like so:
mounted() {
console.log("mounted()")
}
Emitting events
At first, it may seem intuitive to pass a function down to child components as a prop. However, this is a code smell. Why? Because it couples (or bleeds) behavior between components, which may become not so obvious and difficult to maintain in a complete component tree.
Following the pub/sub event model that is so natural to the way the web works (think onclick), vue makes it easy for components to emit events that can be observed by their parents.
In vue 3, the Options API now provides an emits setting.
On the parent component App.vue:
<script>
import UserCard from "./components/user-card.vue";
export default {
components: {
UserCard,
},
data: () => ({
userData: {
name: "Ben Mac",
favoriteFood: "Poke bowl",
},
}),
methods: {
changeName() {
this.userData.name = "Rob Pike";
},
},
};
</script>
<template>
<header>
<div class="wrapper">
<!-- syntactic sugar: ':' is v-bind and '@' is 'v-on' -->
<UserCard :user="userData" @change-name="changeName()" />
</div>
</header>
</template>
On the child component user-card.vue:
<script>
export default {
// defines inputs
props: {
user: {
type: Object,
required: true,
},
},
// defines outputs
emits: ["change-name"],
};
</script>
<template>
<h1>User: {{ user.name }}</h1>
<p>Favorite food: {{ user.favoriteFood }}</p>
<button @click="$emit('change-name')">Change Name</button>
</template>
Event tips:
- The vue devtools have a handy timeline feature, that tracks component events
- The
emitssection in the Options API is new to vue 3, however the core$emitfunction is identical to vue 2. In a nutshell, vue 3 allows you to document the events in a similar way toprops. - The
emitssection, is actually quite powerful, allowing post-event data validation if you choose. See event validation
Slots
Components can accept props, which can be JavaScript values of any type. But how about template content?
<button class="fancy-btn">
<slot></slot>
<!-- slot outlet -->
</button>
The <slot> element is a slot outlet that indicates where the parent-provided slot content should be rendered.
<FancyButton>
Click me!
<!-- slot content -->
</FancyButton>
Fetching data
It common to use lifecycle hooks to perform housekeeping, such as querying and deserialising data from a server. Using the PokeAPI REST API is one convenient way to experiment.
Basic vue life cycles:
mountedcan be thought of when it first becomes visible on the screen (i.e., the DOM is patched)beforeCreatedhappens prior to the Options API being available (i.e., happens very early on)createdtriggers immediately after the Options API environment has been setup for the component, making it a great place to perform background API work that needs to store data into the componentsdatabucket
Unique identifiers
When list rendering it will soon become evident that reactive fragments need unique identifiers. Why? This allows vue to track each reactive component against the DOM.
<li v-for="user in userList">
<- 'Elements in iteration expect to have 'v-bind:key' directives' {{ user.name
}} <em>{{ user.website }}</em>
</li>
To remedy this v-bind a key attribue :key='item.id for short:
<li v-for="user in userList" :key="user.id">
{{ user.name }} <em>{{ user.website }}</em>
</li>
Note, if the source data doesn’t provide a decent unique identifier, checkout the uuid package.
Styling
Vue injects component <style> tags into the main <head> by default, meaning styling gets tossed into a global namespace. Inspecting the head tag with devtools reveals, it really is this simple:
<style
type="text/css"
data-vite-dev-id="C:/Users/ben/git/vue-hack/src/App.vue?vue&type=style&index=0&lang.css"
>
html {
background-color: papayawhip;
}
</style>
How the heck can components sanely style themselves, without bleeding styling across the entire app? Imagine the debugging nightmare, investigating which rules take precidence over the others…
Vue has your back with scoped styles:
<style scoped>
button {
border: 3px turquoise solid;
}
</style>
How?? On your behalf, using PostCSS, Vue will inject a unique v data attribute, like so:
<div data-v-9ad5ab0c="">
<h1 data-v-9ad5ab0c="">Counter</h1>
<p data-v-9ad5ab0c="">10</p>
<button data-v-9ad5ab0c="">Increment Count</button>
<div data-v-9ad5ab0c="">
<h3 data-v-9ad5ab0c="">8</h3>
<label data-v-9ad5ab0c="" for="incrementAmount">Increment by:</label>
<input data-v-9ad5ab0c="" type="number" />
</div>
</div>
And generate a CSS selector against that unique identifer:
button[data-v-9ad5ab0c] {
border: 3px turquoise solid;
}
CSS modules
CSS Modules are a CSS file processing system that allows developers to write modular, reusable, and maintainable CSS code
With CSS Modules, CSS classes are locally scoped to the components where they are used, preventing conflicts with other classes in the global CSS namespace.
In addtion they support dynamic class names, which can be useful when working with complex user interfaces that require conditional rendering or dynamic styling.
The way Vue CSS Modules work, is by using the module attribute on the <style> element. CSS classes defined within are then exposed via the special $style object (note the v-bind colon):
<template>
<button :class="$style.button" @click="$emit('change-name')">
Change Name
</button>
<p>^^^ this button is styled with CSS Modules</p>
</template>
<style module>
.button {
border: 3px solid greenyellow !important;
}
</style>
How the heck do CSS modules work? Inspecting the above <button> instance in the browser, observe this:
._button_5i42q_3 {
border: 3px solid greenyellow !important;
}
Through the brilliance of CSS Modules and its compilation system, can see the components usage of the .button class has been assigned a much more unique name.
CSS v-bind
I hear you say ’no way!?’. How can CSS be tied to reactive data!? Using the CSS variables under the hood, Vue provides a convenient v-bind() function for CSS, that bridges the two worlds of JavaScript and CSS:
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: "red",
};
},
};
</script>
<style>
.text {
color: v-bind(color);
}
</style>
Composition API
In contrast to the Options API, the Composition API defines a component’s logic using imported API functions. Its pure JS and exposes you to Vue’s raw primitives…hence feels more flexible and free.
In SFCs, Composition API is typically used with <script setup>. The setup attribute is a hint that makes Vue perform compile-time transforms that allow us to use Composition API with less boilerplate. For example, imports and top-level variables / functions declared in <script setup> are directly usable in the template.
<script setup>
import { ref, onMounted } from "vue";
// reactive state
const count = ref(0);
// functions that mutate state and trigger updates
function increment() {
count.value++;
}
// lifecycle hooks
onMounted(() => {
console.log(`The initial count is ${count.value}.`);
});
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
Alternatively to using the <script setup> method, its possible to define a setup() function at the top of the export like so:
export default {
async setup() {
const res = await fetch(...)
const posts = await res.json()
return {
posts
}
}
}
If you try to do async work in the Compositon API, will get the following warning:
Component : setup function returned a promise, but no boundary was found in the parent component tree. A component with async setup() must be nested in a in order to be rendered.
What the heck is a Suspense?
Suspense is a built-in component for orchestrating async dependencies in a component tree. It can render a loading state while waiting for multiple nested async dependencies down the component tree to be resolved.
Notice how “parent” was referenced in the above error message. The Suspense needs to be registered in the parent component that houses the component with the async composition API code.
Given App.vue is the parent for me in this case, on App.vue I register the built-in <Suspense> component, setting its default slot and fallback slot:
<template>
<Suspense>
<div>
<header class="header">
<nav class="nav">
<a href="#" @click.prevent="showHomePage">Home</a>
<a href="#" @click.prevent="showLoginPage">Login</a>
<a href="#" @click.prevent="showUserPage">Users</a>
</nav>
</header>
<HomePage v-if="currentPage === 'Home'" />
<UserPage v-else-if="currentPage === 'Users'" />
</div>
<template #fallback> Loading... </template>
</Suspense>
</template>
Reactive refs
The Composition API is vanilla JS, as a result most of the automated comforts that come with the Options API arent applied by default. This goes for reactive data.
Vue exposes reactive data via the ref() and reactive() functions:
<script>
import { computed, ref } from "vue";
export default {
async setup() {
const regionName = ref('kanto'); // a reactive reference
const regionNameAllCaps = computed(
() => {
return regionName.value.toUpperCase();
}
)
}
}
</script>
Here regionName without the use of ref() would be a vanilla JS variable, with no reactive super powers.
The reactivity API exposes all of Vue core primitives, such as computed() for computed props, watch() for watchers and so on.
Script setup
The magic of the Composition API acends to god mode with , which provides compile-time syntactic sugar and is the recommended approach for SFC that are purely based on the Composition API. It involves including a setup attribute to the <script> block like so:
<script setup>
console.log('hello script setup')
const changeRegionName = () => {
regionName.value = 'Hoenn'
}
return {
changeRegionName, //method
}
</script>
Top level bindings (variables, functions, imports) within the <script> tag, are immediately usable in the template.
Vue exposes raw functions for all the primitive behaviours that underpin the Options API, such as reactive ref(), defineProps and defineEmits and so on.
Composables
A independent and reusable JS/TS utility that hinges off the composition API.
For example, the following is defined in src/composables/countStore.js
import { ref } from 'vue'
export const globalCount = ref(100);
const incrementGlobalCount = () => {
globalCount.value += 50
}
export function useCount() {
const localCount = ref(50)
const incrementLocalCount = (amount) => {
localCount.value += amount
}
return {
incrementGlobalCount,
incrementLocalCount,
globalCount,
localCount
}
}
Components that comsume the “composable” simply import like any other piece of JS/TS. Here the BaseButton.vue component makes use of useCount:
import { useCount } from '../composables/countStore'
export default {
setup() {
const countStore = useCount()
return {
countStore
}
},
data: () => ({
incrementAmount: 8
}),
methods: {
incrementCount() {
this.countStore.incrementLocalCount(this.incrementAmount)
}
},
Other components could smilarly use the composable in this way, Vue will manage the state based on standard JS scoping rules.
Vue Router
Vue provides an official router https://router.vuejs.org
It is idiomatic to separate page components from lower level components, in src/views and src/components respectively.
Define the array of routes you would like in an importable piece of JS/TS, such as src/router.js:
import HomePage from './views/HomePage.vue'
import LoginPage from './views/LoginPage.vue'
import UserPage from './views/UserPage.vue'
export const routes = [
{
path: '/',
component: HomePage
},
{
path: '/login',
component: LoginPage
},
{
path: '/user',
component: UserPage
},
]
In main.ts import the routes array and bootstrap it into the router middleware and app. Note the various history modes, below will hook up the HTML 5 “web mode”, see https://router.vuejs.org/guide/essentials/history-mode.html:
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './router.js'
import App from './App.vue'
const router = createRouter({
history: createWebHistory(),
routes
})
const app = createApp(App)
app.use(router)
app.mount('#app')
Then back in App.vue wire in the router directives, which will conditionally render components based on route:
<template>
<Suspense>
<div>
<header class="header">
<nav class="nav">
<router-link to="/">Home</router-link>
<router-link to="/login">Login</router-link>
<router-link to="/user">User</router-link>
</nav>
</header>
<router-view />
</div>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
Dynamic route params
When you need to pass parameters to your routes, vue router provides dynamic route matching. These params are marked with a colon : in the route path defition:
export const routes = [
{
path: '/pokemon/:id',
component: PokemonPage
},
]
Downstream routed components are exposed to route params using the special $route variable:
PreProcessors
The script, template and/or style blocks can each declare specific pre-processor languages by leveraging the lang attribute.
TypeScript for scripts:
<script lang="ts">
// use TypeScript
</script>
Pug for templating:
<template lang="pug"> p {{ msg }} </template>
Sass for styling:
<style lang="scss">
$primary-color: #333;
body {
color: $primary-color;
}
</style>