In this snippet we will discuss about Vue.js 3 composition Api along with the setup() function and composable files.
The Composition Api is the new Api released along with Vue 3 that have the same functionality of the Options Api but using new syntax and Api. In Vue 2 if you remember we tend to work with the Options Api.
Options Api Example:
export default { name: "ExampleComponent", data() { return { counter: 0 } }, computed: { }, mounted() { }, created() { } }
In this sample component using Options Api, we declared data() function that returns our component data, and also some lifecycle hooks like mounted(), created().
The same logic can be written using Vue 3 Composition Api:
export default { name: "ExampleComponent", setup() { const counter = ref(0); const computedProperty = computed(() => {}); onMounted(() => { console.log('component mounted'); }); return { counter } } }
As you see in this example the important piece is the setup() function, this is the Composition Api syntactic sugar. Inside the setup() we write all the component logic like component data, lifecycle hooks and computed properties, and finally the setup() return the data and methods that will be consumed by the component html.
To better understand it we will use a simple example, before this step check your node and npm version in my case i am using node v16.17 and npm v9.7.1
Create a vue 3 app using vite:
npm create vite@latest
You will be asked some questions like project name, framework, like so:
Project name: … vue3-composition-api
Select a framework: › - Use arrow-keys. Return to submit. Vanilla ❯ Vue React Preact Lit Svelte Solid Qwik Others
? Select a variant: › - Use arrow-keys. Return to submit. TypeScript ❯ JavaScript Customize with create-vue ↗ Nuxt ↗
Then go inside the project using cd command:
cd vue3-composition-pi
install the dependencies:
npm install
If you check the package.json:
"dependencies": { "vue": "^3.3.4" }, "devDependencies": { "@vitejs/plugin-vue": "^4.2.3", "vite": "^4.4.5" }
As you see here the vue version is vue 3.3.4 and also “@vitejs/plugin-vue”, “vite” packages installed because we are using vite.
The next step is to launch the app:
npm run dev
Check your project is running in the browser by clicking the project url from the terminal, open the project in your IDE,
we will create a simple component using options Api and then later we will migrate this component to composition Api.
Inside of the components/ folder create this component:
NumericInput.vue
<template> <label>Enter amount</label> <div class="input-wrapper"> <span class="operator" @click="decrement">-</span> <input type="text" name="amount" inputmode="numeric" pattern="[0-9]+" v-model="amount" /> <span class="operator" @click="increment">+</span> </div> </template> <script> export default { name: "NumericInput", data: () => ({ amount: 0 }), methods: { increment() { if(isNaN(this.amount)) { this.amount = 0; } this.amount++; }, decrement() { if(isNaN(this.amount)) { this.amount = 0; } this.amount--; } }, watch: { amount(newVal, oldVal) { if(isNaN(newVal)) { this.amount = 0; } } } } </script> <style scoped> .input-wrapper { height: 37px; display: flex; } input { padding: 2px 3px; font-size: 20px; color: #504d4d; text-align: center; } .operator { padding: 2px 3px; background: #ccc; width: 27px; display: inline-block; font-size: 23px; font-weight: bold; cursor: pointer; } </style>
This is a custom number input component with two buttons for increment and decrement.
To see this component in action open App.vue and modify it like so:
<script setup> import NumericInput from './components/NumericInput.vue' </script> <template> <NumericInput /> </template>
If you return back to the browser you will see the component and you can click any buttons to increment or decrement.
Now we will migrate this component to use Composition Api, note that for the component to use Composition Api you can use any of these methods:
- First Method: <script setup>
In this method you will specify “setup” keyword along with <script> tag and it indicates that you are using Composition Api, all your code logic will be there:
<script setup> import { ref } from 'vue' </script>
- Second Method: setup() function
<script> import { ref } from 'vue'; export default { setup() { // your code return { } } } </script>
Any of these methods will do the job, in our case let’s use the second method:
NumericInput.vue (Composition Api Version):
<script> import {ref, watch} from "vue"; export default { name: "NumericInput", setup() { const amount = ref(0); const increment = () => { if(isNaN(amount.value)) { amount.value = 0; } amount.value++; } const decrement = () => { if(isNaN(amount.value)) { amount.value = 0; } amount.value--; } watch(amount, (newVal, oldVal) => { if(isNaN(newVal)) { amount.value = 0; } }) return { amount, increment, decrement } } } </script>
In this code i replaced the Options Api based code to be inside the setup() function Composition Api. The setup() function should return an object that contain all the state, methods, reactive properties that should be exposed to the component template, in the example i returned the {amount, increment, decrement}.
As a rule of thumb when thinking about converting any component that uses the Options Api, these steps should be implemented:
- data() function: State declared inside the data() function will be reactive properties, vue 3 provide use some Api’s to declare reactive state such as the ref() or reactive() functions.
There are some differences between ref() and reactive(), refer to vue 3 reactivity section to learn more. In this example i am using ref() for the amount property and provided an initial value of 0. State declared using ref() must be referenced using .value inside of setup(), as in the increment() function:
amount.value++;
- methods: Second the methods in the options Api will be declared as regular functions inside of setup(), as you see the increment() and decrement() functions.
- watchers: Vue 3 also provide us some Api’s to deal with watchers as in the options api.
From these Api’s the watch() and watchEffect(). In the above example i imported the watch() function to monitor the amount property. watch() which accepts the reactive property to watch declared using ref() or reactive() or computed() or getter function and a callback function and it works the same as in options api:
watch(amount, (newVal, oldVal) => { if(isNaN(newVal)) { amount.value = 0; } })
watching getter:
// getter watch( () => x.value + y.value, (sum) => { console.log(`sum of x + y is: ${sum}`) } )
- lifecycle hooks: vuejs 3 provides Api’s for handling lifecycle hooks like mounted, created, etc. These hooks usually named as “on<hook name>”, for example onMounted(), onUpdated, ,onUnmounted, etc. Refer to vue 3 hooks docs to see the order of each lifecycle hook.
To use particular hook just import it first, then call the hook, for example let’s set the amount property to be 5 at the first loading of the app:
import {ref, watch, onMounted} from "vue";
Inside of setup():
onMounted(()=> { amount.value = 5; });
- Computed Properties: We already used computed properties in the options Api, the same concept applies here as Vue 3 also provide the computed() function to declare computed properties.
In the simplest case computed() accepts a callback to return a value, this is just a getter():
import {ref, watch, onMounted, computed} from "vue";
setup() { .... .... const isReachedMaximum = computed(() => { return amount.value >= 100 ? 'You have reached the maximum amount' : ''; }); return { ..., .... ... isReachedMaximum } }
<template> <label>Enter amount</label> <div class="input-wrapper"> <span class="operator" @click="decrement">-</span> <input type="text" name="amount" inputmode="numeric" pattern="[0-9]+" v-model="amount" /> <span class="operator" @click="increment">+</span> </div> <p>{{isReachedMaximum}}</p> </template>
Computed properties also can be writable which accepts a setter and getter, in this case it will accept an object with two methods getter and setter:
const computedProp = computed({ get() { return '' }, set(value) { } });
Extract the setup() function logic to a Composable File:
The main target of the Composition Api is code reuse-ability in multiple components. For this purpose whenever you using Vue 3 composition api you should consider writing your logic in composable file as this provides a clean reusable and maintainable code.
Create a new directory in the src/ dir called composables/ and inside it create this file useNumericInput.js
src/composables/useNumericInput.js
import {ref, watch, onMounted, computed} from "vue"; export function useNumericInput() { return { } }
You can name the composable file anything you want but as a convention it should be prefixed with “use” then the rest of the name like “useMouse.js”, “useNavigator.js” depending on functionality of the composable, here i am calling it “useNumericInput.js”.
Next the composable should export a function that returns an object with the necessary data that will be consumed in the component the same as the setup() function return value.
export function useNumericInput() { }
Let’s copy all the code in the component setup() function to the composable function:
useNumericInput.js
import {ref, watch, onMounted, computed} from "vue"; export function useNumericInput() { const amount = ref(0); const increment = () => { if(isNaN(amount.value)) { amount.value = 0; } amount.value++; } const decrement = () => { if(isNaN(amount.value)) { amount.value = 0; } amount.value--; } watch(amount, (newVal, oldVal) => { if(isNaN(newVal)) { amount.value = 0; } }) const isReachedMaximum = computed(() => { return amount.value >= 100 ? 'You have reached the maximum amount' : ''; }); const computedProp = computed({ get() { return '' }, set(value) { } }); onMounted(()=> { amount.value = 5; }); return { amount, increment, decrement, isReachedMaximum } }
That’s it, don’t worry all this code including reactive state, lifecycle hooks, computed refs, watchers will work as expected.
Now refactor the <NumericInput /> component to use the composable file:
<script> import {useNumericInput} from "../composables/useNumericInput.js"; export default { name: "NumericInput", setup() { const {amount, increment, decrement, isReachedMaximum} = useNumericInput(); return { amount, increment, decrement, isReachedMaximum } } } </script>
Now if you refresh the app you will see that the component work normally without any problems.
As an example of some helpful utilities written using Composition Api can be found on VueUse.com website, it contain a lot of helpful composables that can be used in your project instead of writing them from scratch.