Skip to content

Vulk and Vue 3

Reuse stateful logic with composable

Composables are reusable pieces of code that handle stateful logic, using functions from the Reactivity API and Composition API. They are only meant to be called synchronously in a <script setup lang="ts"> context. Read more about on the Vue.js documentation website: https://vuejs.org/guide/extras/composition-api-faq.html

Here are some examples of composables from the @vueuse/core library used in Vulk:

You can also find internal composables in ./src/composable, such useLayout
Also a look at the patterns and tips for writing good composable slides by antfu, author of VueUse.

Why the Vue 3 Composition API?

Reactive data stores with Pinia

When you need to share data across the entire application, you can use Pinia library, which replaces Vuex in the Vue 3 ecosystem.

When to use Pinia stores:

  • When you need to share data everywhere in your application, with one source of truth.
  • When you need to keep data in all app lifecycle (ex: between pages transitions).

When not to use stores:

  • When you need to share data in only a component tree (ex: a single page), use provide/inject instead.

Let's see how we can create a simple store to handle the user session data:

ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { useStorage } from "@vueuse/core";

export type UserData = Record<string, any> | null;

export const useUserSession = defineStore("userSession", () => {
  const token = useStorage("token", "");
  const user = ref<Partial<UserData>>();
  const loading = ref(true);

  const isLoggedIn = computed(
    () => token.value !== undefined && token.value !== ""
  );

  function setUser(value: Partial<UserData>) {
    user.value = value;
  }

  function setToken(value: string) {
    token.value = value;
  }

  function setLoading(value: boolean) {
    loading.value = value;
  }

  async function logoutUser() {
    token.value = "";
    user.value = undefined;
  }

  return {
    user,
    token,
    isLoggedIn,
    loading,
    logoutUser,
    setUser,
    setToken,
    setLoading,
  } as const;
});

We can now use the store everywhere in our application:

vue
<script setup lang="ts">
import { watch } from "vue";
import { useUserSession } from "/@src/stores/userSession";

const userSession = useUserSession();

watch(userSession.isLoggedIn, () => {
  console.log("login status changed", userSession.isLoggedIn);
});
</script>

<template>
  <div v-if="userSession.isLoggedIn">
    Welcome {{ userSession.user.name }} -
    <Button @click="userSession.logoutUser">Logout</Button>
  </div>
  <div v-else>You are not logged in</div>
</template>

TIP

read the pinia documentation on the official website:
https://pinia.vuejs.org/

Advanced router management

Head meta tags

In Vulk, you can easily setup page meta tags using composables provided by unhead, like useHead or useSeoMeta.

WARNING

You need to use either SSG or SSR in order to have meta tags rendered for each page. In case you can not use SSG or SSR, you have to set your head meta tags manually in the index.html file, but you will loose the ability to have different meta tags for each page.

vue
<script setup lang="ts">
useHead({
  templateParams: {
    site: {
      name: 'My Site',
      url: 'https://example.com',
    },
    separator: '-',
  },
  title: 'My page',
  titleTemplate: '%s %separator %site.name',
  meta: [
    {
      property: 'og:site_name',
      content: '%site.name',
    },
    {
      name: 'description',
      content: 'My page description',
    },
  ],
  link: [
    {
      rel: 'stylesheet',
      href: 'https://fonts.googleapis.com/css2?family=Roboto+Condensed&display=swap',
    },
  ],
})
</script>

In Vulk, you can easily setup the router navigation guards by creating a src/plugins/navigation-guards.ts file:

ts
import { START_LOCATION } from "vue-router";

import { definePlugin } from "/@src/app";
import { useUserSession } from "/@src/stores/userSession";

export default definePlugin(({ router, pinia }) => {
  router.beforeEach(async (to, from) => {
    // we can access our pinia store from here
    // the pinia argument is optional but it is recommended
    // to use it when dealing with SSR, because we are not in a Vue instance yet
    const userSession = useUserSession(pinia);

    // if this is the first time the user is accessing the app, and has a token,
    if (from === START_LOCATION && userSession.isLoggedIn) {
      // check user token validity, we can perform async operations here

      // explicitly return false to cancel the navigation
      return false;
    }

    // if the page we are navigating to requires authentication, and the user is not logged in,
    // redirect to the login page
    if (to.meta.requiresAuth && !userSession.isLoggedIn) {
      // explicitly return another route to redirect the user
      return {
        name: "auth",
        query: { redirect: to.fullPath },
      };
    }
  });
});

Adding metadata to routes

You might have noticed that we used the meta property of the to route. This is a great feature to add metadata to routes, such as requiresAuth, layout or anything else that is arbitrary information about routes. Thanks to vite-plugin-pages, we can simply add a new root <route> element to our *.vue files located in the ./src/pages directory

Let's take the blog example from the Setup Your Project - Creating new pages section.

First case: setting metadata to a single route

We want to add a new page accessible at /profile for user profile, which will be only accessible to logged in users, while the rest of the website will be accessible to all users:

bash
 my-vulk-quickstarter-project/
 ├── src/
   ├── pages/
   ├── profile/           # profile nested routes
   └── index.vue      # profile page accessible at `/profile
   └── profile.vue        # profile nested routes wrapper, should contain a `<RouterView />`

In the src/pages/profile/index.vue file, we add the requiresAuth metadata to the route:

vue
<route lang="yaml">
meta:
  requiresAuth: true
</route>

<script setup lang="ts">
// the profile script
</script>

<template>
  <!-- the profile template -->
</template>

Second case: setting metadata to nested routes

We want to restrict our entire /profile/* pages to logged in users:

bash
 my-vulk-quickstarter-project/
 ├── src/
   ├── pages/
   ├── profile/           # profile nested routes
   ├── index.vue      # the profile page accessible at `/profile/`
   └── stats.vue      # the profile stats page accessible at `/profile/stats`
   └── profile.vue        # profile nested routes wrapper, should contain a `<RouterView />`

In the src/profile/stats.vue file, we add the requiresAuth metadata to the wrapper route:

vue
<route lang="yaml">
meta:
  requiresAuth: true
</route>

<script setup lang="ts">
// we do not need to so anything special here, but we can!
</script>

<template>
  <div>
    <RouterView />
  <div>
</template>

Advanced vue customization

Using VueUse

Vueuse is a collection of essential Vue Composition API utilities for building your next Vue application.

We recommend you to use this library to avoid reinventing the wheel, the library is well documented and has a lot of useful features, to name a few:

TIP

read the VueUse documentation on the official website:
https://vueuse.org/

Register global components

In Vulk, you can easily setup global components and plugins in the src/plugins folder:

ts
import { definePlugin } from "/@src/app";

export default definePlugin(({ app }) => {
  // you can load libraries that won't work on server side asynchronously
  if (!import.meta.env.SSR) {
    import("client-only-library").then((pkg) => {
      // ... do stuff with the module
      app.use(pkg.default);
    });
  }
});

Vue and Typescript?

Typescript is just an extension of Javascript, If you are new to it, don't be afraid because all valid JavaScript code is also TypeScript code.(TypeScript documentation)
The main advantages of using Typescript are:

  • Validates your code ahead of time
  • Provides auto-completion
  • Supports complex Type checking

Vulk is fully compatible with Typescript. Therefore, you will gain the benefits of improved type checking and code completion! If you use typescript notation in all of your components, you will be able to run the vue-tsc compiler to check your application for errors.

Let's create a simple MyCustomInput with Typescript features:

vue
<script setup lang="ts">
export interface MyCustomInputProps {
  modelValue: string;
  label?: string;
  id?: string;
  disabled?: boolean;
}
export interface MyCustomInputEmits {
  (e: "update:modelValue", value: string): void;
  (e: "disable"): void;
}

// withDefaults and defineProps are global helpers from @vue/sfc-compiler
const props = withDefaults(defineProps<MyCustomInputProps>(), {
  label: undefined,
  id: undefined,
});

// defineEmits is global helpers from @vue/sfc-compiler
const emit = defineEmits<MyCustomInputEmits>();

// this is our method that emits the event
function onInputChanged(event: Event) {
  emit("update:modelValue", (event.target as HTMLInputElement).value);
}
</script>

<template>
  <label v-if="props.label" :for="props.id">
    {{ props.label }}
  </label>

  <input
    :value="props.modelValue"
    :disabled="props.disabled"
    @change="onInputChanged"
  />

  <VButton v-if="!props.disabled" @click="emit('disable')">disable me!</VButton>
</template>

We can the verify that our component is working as expected, simply load in another component

vue
<template>
  <MyCustomInput :model-value="42" />
</template>

To confirm that the vue-tsc compiler is working, run the following command:

bash
pnpm test:tsc

Note that the test:tsc command will fail, as we have set a number in the :model-value prop.

It should output the following error:

bash
src/pages/index.vue:13:23 - error TS2322: Type 'number' is not assignable to type 'string'.

13       <MyCustomInput :model-value="42" />

WARNING

On large applications, test:tsc may run out of memory. You can increase the memory limit by running the following command:

bash
NODE_OPTIONS=--max_old_space_size=4096 pnpm test:tsc

Keep it clean with linters

Because we love clean code, we have configured 4 linters which have their own purpose:

  • eslint: prevent code quality concerns (no unused vars, ...)
  • stylelint: prevent CSS quality concerns (no invalid colors, ...)
  • commitlint: handles formatting commit messages

You can check the quality of your code by running

bash
pnpm test:lint

Linters can fix a lot of issues all by themselves. To do so, try running

bash
pnpm lint

VSCode users

Install recommended extensions, linting will then occur each time files are saved!

All Rights Reserved