Creating Custom Hooks with Vue 3 & Typescript
TypeScript offers a static type system that can help to prevent many potential runtime errors as applications grow, this is why Vue 3 is built from the ground in TypeScript. This means you don’t need any additional tooling to use TypeScript with Vue because it has full typescript support.
This article will take you on a step-by-step guide to creating a Vue 3 app with typescript support, adding TypeScript support to an existing Vue 3 app, and finally build a custom hook with vue-class-component.
Prerequisite
Lets start by creating a vue 3 app with typescript support using the vue-CLI tool.
Install vue-cli by executing the following command:
npm install --global @vue/cli
If you have the CLI already you can check if you have the right version:
vue --version
Make sure you upgrade it to the latest version with the following command:
npm update -g @vue/cli
# OR
yarn global upgrade --latest @vue/cli
Create a new Vue 3 app with TypeScript support
vue create vue-ts-project
Choose the manually select feature option
Hit the space key to select the following options:
- choose Vue version
- Babel
- TypeScript
- Linter / Formatter
Next Choose Vue 3.x(Preview) as the version for the project.
- Enter yes to use class-style component syntax
- Enter yes to use Babel alongside TypeScript
- Select any linter of your choice
After the Vue-CLI successfully scafolds the project, we will have a Vue3 project with full typescript support.
Execute the following command to serve the newly created project in the browser:
cd vue-ts-project
npm run serve
Adding TypeScript to an Existing Vue 3 Application
If you already have a Vue CLI project without TypeScript execute the following command to add typescript support:
vue add typescript
Make sure that script
part of the component has TypeScript set as a language:
<script lang="ts">
...
</script>
Now you can write TypeScript in vue components within your application.
Build Custom Hook with vue Class Component
Before we continue, the vue official docs recommends using IDE such as VS code since it has great support for typescript.
With our Vue 3 Project completely setup, Lets get into building a custom hook.
We will be building two composable hooks:
- The first hook will be used to interact directly to a rest API
- The second hook will depend on the first
Finally we will add type safety to the project.
Create a folder called hooks with a file name api.ts in the src directory and add the code snippet below:
import { ref, Ref } from "vue";
export default function useApi (url, options){
const response = ref();
const request = async () => {
const res = await fetch(url, options);
const data = await res.json();
response.value = data;
};
return { response, request };
}
Since our project is setup with typescript, some piece of our code will be highlighted with red color signifying type errors.
Why do we have the error highlight
If you hover on fetch, you will notice that it is a function that accepts two parameters the first parameter expects a type of RequestInfo while the second parameter expects an optional type of RequestInit or undefined:
function fetch(input: RequestInfo, init?: RequestInit | undefined): Promise<Response>
Also hovering on url and option parameters in the useApi function you will notice that they currently have the type of any which does not match the type of the fetch function which they are used. Hence the typescript linter notifies with the error highlight.
(parameter) url: any
Parameter 'url' implicitly has an 'any' type.ts(7006)
Handling the Error
We will handle this error by defining the types for the url and option parameters.
If you are using vs code, hovering on any of the highlighted parameters will suggest a quick fix which will automatically define the types for these parameters as follows:
import { ref } from "vue";
export default function useApi (url: RequestInfo, options?: RequestInit | undefined){
const response = ref();
const request = async () => {
const res = await fetch(url, options);
const data = await res.json();
response.value = data;
};
return { response, request };
}
Add the ?
symbol to the options
parameter to notify the typeScript engine that options parameter will be optional.
Next, we will create our second hook which will depend on the useApi hook to make request.
In the hooks folder, create a users.ts file with the following:
import useApi from "./api";
import { ref } from "vue";
export default async function useProducts(){
const { response: products, request } = useApi(
"https://ecomm-products.modus.workers.dev/"
);
const loaded = ref(false);
if (loaded.value === false) {
await request();
loaded.value = true;
}
return { products };
}
we import the useApi hook from the api.ts file and ref from vue. ref
takes an inner value and returns a reactive and mutable ref object. The ref object has a single property .value
that points to the inner value.
We pass in the API endpoint to useApi hook. We destructured the response and request from the useApi hook for making API request. The ref object is used to set the initial value of loaded to false. We renamed response to product and finally return products as an object.
Use the Custom Hook
In the components folder create a Users.vue file and add the code below:
<template>
<div>
<h3>Customers</h3>
<table id="customers" >
<tr>
<th>ID</th>
<th>NAME</th>
<th>USERNAME</th>
<th>EMAIL</th>
<th>ADDRESS</th>
<th>PHONE</th>
<th>WEBSITE</th>
</tr>
<tr v-for="user in users" :key="user.id">
<td>{{user.id}}</td>
<td>{{user.name}}</td>
<td>{{user.username}}</td>
<td>{{user.email}}</td>
<td>{{user.address.street}}</td>
<td>{{user.phone}}</td>
<td>{{user.website}}</td>
</tr>
</table>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import useUsers from "@/hooks/users";
export default defineComponent({
name: "Users",
async setup() {
const { users } = await useUsers();
return { users };
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#customers {
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
border-collapse: collapse;
width: 100%;
}
#customers td, #customers th {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
#customers tr:nth-child(even){background-color: #f2f2f2;}
#customers tr:hover {background-color: #ddd;}
#customers th {
padding-top: 12px;
padding-bottom: 12px;
text-align: left;
background-color: #4CAF50;
color: white;
}
</style>
The users component takes advantage of the composition API. Basically this component fetches the list of users via the useUser
hook and displays the users in the template.
Update App.vue with the following:
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<Suspense>
<template #default>
<Users />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Users from "./components/Users.vue";
export default defineComponent({
components: {
Users
}
});
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Vue 3 offers the Suspense component which will manage ascynchronous data fetching process, with a default view once the data is loading, and a fallback view when the data is being loaded all without the need for custom code. The new Vue composition API will understand the current state of our component, so it will be able to differentiate if our component is loading or if it’s ready to be displayed.
Improving the Type Safety
There is a need to define types for the API response data because response data could be unpredictable at times. So users.ts should be updated as follows:
import useApi from "./api";
import { Ref, ref } from "vue";
export interface Location {
lat: number;
lng: number;
}
export interface Address {
street: string;
suite: string;
city: string;
zipcode: number;
geo: Location;
}
export interface User {
id: string;
name: string;
username: string;
email: string;
address: Address;
}
export default async function useUserss() {
const { response: users, request } = useApi<User[]>(
"https://jsonplaceholder.typicode.com/users"
);
const loaded = ref(false);
if (loaded.value === false) {
await request();
loaded.value = true;
}
return { users };
}
Conclusion
We have reached the end of this article, and we have learned how to set up vue 3 projects with typescript using the vue-CLI tool, adding typescript to an existing vue 3 project, and finally build a custom hook with vue 3 and typescript.