Skip to content

Vuero 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.You may want to 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 Vuero:

You can also find internal composables in ./src/composable, such as useNotyf, useFetch and useDropdown

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
// src/stores/userSession.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", () => {
  // token will be synced with local storage
  // @see https://vueuse.org/core/useSessionStorage/
  const token = useSessionStorage("token", "");

  // we use ref and computed to handle reactive data
  const user = ref<Partial<UserData>>();
  const loading = ref(true);
  const isLoggedIn = computed(
    () => token.value !== undefined && token.value !== ""
  );

  // but we also declare functions to alter the state
  function setUser(newUser: Partial<UserData>) {
    user.value = newUser;
  }
  function setToken(newToken: string) {
    token.value = newToken;
  }
  function setLoading(newLoading: boolean) {
    loading.value = newLoading;
  }
  async function logoutUser() {
    token.value = undefined;
    user.value = undefined;
  }

  // we return the entire store
  return {
    // this is our state
    user,
    token,
    isLoggedIn,
    // and our mutations
    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 }} -
    <VButton @click="userSession.logoutUser">Logout</VButton>
  </div>
  <div v-else>You are not logged in</div>
</template>

TIP

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

Vuero Plugins

You can create plugins for Vuero simply by creating *.ts files in src/plugins directory.

All plugins will be registered automatically before the app runs.

Here is a plugin boilerplate:

ts
import { definePlugin } from '/@src/app'

export default definePlugin(async ({ app, api, router, head, pinia }) => {
  // run your plugin code here

  // If you need to perform conditional logic based on SSR vs. Client Only, you can use
  if (import.meta.env.SSR) {
    // ... server only logic
  }

  // You can do the same for Client Only logic
  if (!import.meta.env.SSR) {
    // ... client only logic

    // you can lazyload libraries that won't work on server side asynchronously
    import('client-only-library').then((module) => {
      // ... do stuff with the module
    })
  }
})

TIP

This is a good place to fetch user info / global settings prior the app starts

Advanced router management

Head meta tags

In Vuero, 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>

Vue Router Data Loaders

Experimental

Instead of loading data within the component lifecycle, you can use new defineLoader helper function introduced by the vue-router author in this official Vue RFC: https://github.com/vuejs/rfcs/discussions/460

Keep in mind this is an experimental feature from vue-router that may change in future. Feel free to contribute to the RFC's discussions if you have any feedback!

This is an example on how to use defineLoader in a page component (ex: ./src/pages/article/[slug].vue)

vue
<script lang="ts">
// note that we are not in a <script setup> context here
import { defineLoader } from 'vue-router/auto'
import { useFetch } from '/@src/composable/useFetch'

interface Article {
  id: string
  title: string
  slug: string
  content: string
  comments: string[]
}

export const useArticle = defineLoader(async (route) => {
  const slug = (route.params?.slug as string) ?? ''
  const $fetch = useFetch()

  const data = await $fetch<Article[]>(`/articles`, {
    query: {
      slug,
    }
  })
  const article = data?.[0]

  return article
})
</script>

<script setup lang="ts">
const { data: article, pending } = useArticle()

const router = useRouter()

if (!article.value) {
  // If the article does not exist, we replace the route to the 404 page
  // we also pass the original url to the 404 page as a query parameter
  // http://localhost:3000/article-not-found?original=/blog/a-fake-slug
  router.replace({
    name: '/[...all]', // this will match the ./src/pages/[...all].vue route
    params: {
      all: 'article-not-found',
    },
    query: {
      original: router.currentRoute.value.fullPath,
    },
  })
}

// Setup our page meta with our article data
useHead({
  title: computed(() => article.value?.title ?? 'Loading article...'),
})
</script>

<template>
  <LandingLayout theme="light">
    <div v-if="pending">Loading article...</div>
    <div v-else class="blog-detail-wrapper">
      <!--
        Page content goes here

        You can see more complete pages content samples from 
        files in /src/components/pages directory
      -->

      <h1>{{ article?.title }}</h1>
      <div>{{ article?.content }}</div>
    </div>
  </LandingLayout>
</template>

<style lang="scss" scoped>
.blog-detail-wrapper {
  // Here we can add custom styles for the blog page
  // They will be only applied to this component
}
</style>

In Vuero, you can easily setup the router navigation guards by creating a plugin.

ts
import { definePlugin } from '/@src/app'
import { useUserSession } from '/@src/stores/userSession'

export default definePlugin(({ router, api, pinia }) => {
   const userSession = useUserSession(pinia)

  // 1. Check token validity before the app start
  if (userSession.isLoggedIn) {
    try {
      // Do api request call to retreive user profile.
      // Note that the api requests the json-server
      const { data: user } = await api.get('/api/users/me')
      userSession.setUser(user)
    } catch (err) {
      // delete stored token if it fails
      userSession.logoutUser()
    }
  }

  router.beforeEach((to) => {
    // 2. If the page requires auth, check if user is logged in
    // if not, redirect to login page.
    if (to.meta.requiresAuth && !userSession.isLoggedIn) {
      return {
        name: '/auth/login',
        // save the location we were at to come back later
        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 unplugin-vue-router, 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 /blog/new for article creation, which will be only accessible to logged in users, while the rest of the blog will be accessible to all users:

bash
 my-vuero-quickstarter-project/
 ├── src/
   ├── pages/
   ├── blog/           // blog nested routes
   ├── new.vue     // the article creation page accessible at `/blog/new` // [!code ++]
   ├── index.vue   // the articles listing page accessible at `/blog/`
   └── [slug].vue  // the article detail page accessible at `/blog/:slug`
   └── blog.vue        // blog nested routes wrapper, should contain a `<RouterView />`

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

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

<script setup lang="ts">
// the article creation script
</script>

<template>
  <!-- the article creation template -->
</template>

Second case: setting metadata to nested routes

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

bash
 my-vuero-quickstarter-project/
 ├── src/
   ├── pages/
   ├── blog/           // blog nested routes
   ├── index.vue   // the articles listing page accessible at `/blog/`
   └── [slug].vue  // the article detail page accessible at `/blog/:slug`
   └── blog.vue        // blog nested routes wrapper, should contain a `<RouterView />` // [!code ++]

In the src/pages/blog.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>
  <LandingLayout theme="light">
    <RouterView />
  </LandingLayout>
</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 Vuero, you can easily setup global components or extend the global vue scope by creating a plugin. We recommends you to create a single plugin by import, for instance one for vue-tippy, another for v-calendar, etc...

ts
import { plugin as VueTippy } from "vue-tippy";

import { definePlugin } from '/@src/app'

export default definePlugin(({ app }) => {
  // register the vue-tippy plugin globally
  app.use(VueTippy, {
    component: "Tippy",
    defaultProps: {
      theme: "light",
    },
  });

  // register global components
  // here we use defineAsyncComponent, so the component will be lazyloaded
  app.component(
    "VCalendar",
    defineAsyncComponent({
      loader: () => import("v-calendar").then((mod) => mod.Calendar),
      delay: 0,
      suspensible: false,
    })
  );

  // register global directives
  app.directive("focus", {
    mounted: (el) => el.focus(),
  });
}

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

Vuero 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, ...)
  • prettier: handles formatting rules (max line length, ...)
  • 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