A bunch of (scattered) tips and resources as I experiment with Vue.

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:

General wisdom

  • It’s best to stick to conventions of the web and use camelCase in your script and kebab-case in your template
  • Don’t pass functions as props, instead emit 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 the v-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):

vue component anatomy

Events

Event Handling Docs

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 to props.
  • 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 components data 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&amp;type=style&amp;index=0&amp;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