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:
- useCssVar: https://vueuse.org/core/useCssVar
- useElementVisibility: https://vueuse.org/core/useElementVisibility
- useWindowScroll: https://vueuse.org/core/useWindowScroll
- debouncedWatch: https://vueuse.org/core/debouncedWatch
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?
TIP
View the full course available on Vue Mastery:
https://www.vuemastery.com/courses/vue-3-essentials/why-the-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:
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:
<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.
<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>
Navigation guards
In Vulk, you can easily setup the router navigation guards by creating a src/plugins/navigation-guards.ts
file:
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:
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:
<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:
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:
<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:
useShare
: Reactive Web Share API.useWebNotification
: The Web Notification interface of the Notifications APIonClickOutside
: Listen for clicks outside of an elementuseIntersectionObserver
: Detects that a target element's visibility.
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:
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?
TIP
View the full course available on Vue Mastery:
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:
<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
<template>
<MyCustomInput :model-value="42" />
</template>
To confirm that the vue-tsc
compiler is working, run the following command:
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:
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:
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
pnpm test:lint
Linters can fix a lot of issues all by themselves. To do so, try running
pnpm lint
VSCode users
Install recommended extensions, linting will then occur each time files are saved!