Vue
A bunch of (scattered) tips and resources as I experiment with Vue.
- Basics:
General wisdom
Anatomy
Eventhandling
Watchers
Computed props
- Components:
Components
Props
Lifecycle hooks
Emitting events
Slots
- Fetching Data:
Calling APIs in hooks
Unique identifiers
- Styling Components:
Global vs scoped styles
CSS modules
CSS v-bind
- Composition API:
Composition API
Reactive references
script setup
Composables
- Routing and Deployment:
Vue Router
History
Dynamic routes
Deployment
- Advanced:
Pre-processors
Pinia 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
camelCase
in your script andkebab-case
in your template - Don’t pass functions as
props
, insteademit
events props
couples 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-if
directive will destroy elements from the DOM as the condition is toggled (potentially performance expensive depending on the scenario), if desired thev-show
directive 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
emits
section in the Options API is new to vue 3, however the core$emit
function is identical to vue 2. In a nutshell, vue 3 allows you to document the events in a similar way toprops
. - The
emits
section, 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:
mounted
can be thought of when it first becomes visible on the screen (i.e., the DOM is patched)beforeCreated
happens prior to the Options API being available (i.e., happens very early on)created
triggers 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 componentsdata
bucket
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>