Composables
Often in an application you will want to update the state of your application in a reusable way.
To start off with a simple example, we will use a counter.
Create a Simple Composable
A normal counter looks like this:
<template> <div> <p> {{ number }} </p> <button @click="increment"> increment </button> </div></template><script setup>import { ref } from "vue";let number = ref(0);function increment() { number.value += 1}</script>
We make number a reactive variable using ref. Then we call the increment function to increase the number by one. Because it is reactive, it triggers a rerender every time the value changes. Instead of this, we can use a composable.
A composable is logic extrapolated away from the component or page. Usually, we want to use this code many times throughout an application which is why composables are useful. It allows us to create code that is thoroughly tested so we can trust the output of its result. In this case, to increment numbers!
Composable/incrementNumber.js:
import { ref } from "vue";export default function useIncrement() { let number = ref(0); function increment() { number.value += 1; } return { number, increment };}
Notice how this is a JavaScript file, not a Vue file. We create a reactive variable using ref called number. Then we create the function to increment it. But we don’t increment it here. It just contains the variable and the logic. We return both the number and the increment function.
pages/CounterPage.vue:
<template> <div> <p> {{ number }} </p> <button @click="increment"> increment </button> </div></template><script setup>import useIncrement from "@/composables/incrementNumber.js";let {number, increment} = useIncrement();</script>
Now we import the composable to this page. To instantiate it, we destructure both the number and the function out of the composable which gives us access to now use them.
As you can see in the template, we call both number and increment. When we click the button, the number will be incremented.
Now the variable and the function are located in the component because of the import.
Adding a Composable Parameter
You can also add a parameter to change the increment value.
Composable:
import { ref } from "vue";export default function useIncrement(incrementBy=1) { let number = ref(0); function increment() { number.value += incrementBy; } return { number, increment }}
The incrementBy parameter will start as 1 unless we add another number to it. This way, if we forget to add a parameters when calling the composable, it will still work.
Page:
<template> <div> <p> {{ number }} </p> <button @click="increment"> increment </button> </div></template><script setup>import useIncrement from "@/composables/incrementNumber.js";let {number, increment} = useIncrement(2);</script>
Here we add 2 to the useIncrement composable. Now our number increments by 2 instead of 1.
Composables for Calculations
Now let’s take a more complicated example.
In this case, we are going to calculate tax. To keep it simple, we only have 2 tax brackets: (1) less than $40,000 and (2) more than or equal to $40,001. The lower tax bracket will be taxed at 15% and the higher tax bracket 25%.
Here is the composable (composable/caculateTax.js):
import { ref } from "vue";export default function useCalculateIncomeTax() { let leftOverIncome = ref(0); let totalTax = ref(0); function calculateIncomeTax(income) { let lowerTaxRate = 0.15; let higherTaxRate = 0.25; if (income < 40000) { leftOverIncome.value = income - (income * lowerTaxRate); totalTax.value = income * lowerTaxRate; } else { leftOverIncome.value = income - (income * higherTaxRate); totalTax.value = income * higherTaxRate; } } return {leftOverIncome, totalTax, calculateIncomeTax}}
First we import ref for reactive variables so we can trigger rerenders. Then we declare our 2 reactive variables, leftOverIncome (how much money is left after tax) and totalTax (how much is taxed from our total income).
Then we create the function, calculateIncomeTax and insert an income parameter. Remember, this function will exist in the component. Right now, we are not inserting a parameter but just letting the file know, a parameter will be inserted.
We establish the tax rates via lowerIncomeTaxRate and higherTaxRate.
Then, using if-else logic, we calculate how much income is left over and how much the total tax is using basic mathematics.
At the end, we return the two reactive variables and our function.
Now in the component (TaxPage.vue):
<template> <p> Income remaining: {{ leftOverIncome }}</p> <p> Total tax:{{ totalTax }}</p> <input v-model="income" /> <button @click="calculateIncomeTax(income)"> Print </button></template><script setup>import useCalculateIncomeTax from "@/composables/calculateTax.js";import { ref } from "vue";let income = ref(0);let { leftOverIncome, totalTax, calculateIncomeTax} = useCalculateIncomeTax();</script>
In the script, we call our composable and import ref.
We then declare income as a reactive variable. You can see that we have an input binded to income using v-model.
Just incase you need a refresher, v-model binds data to inputs so when we change the input, the income variable is also updated.
We then use destructuring to get the leftOverIncome and totalTax variables, and the function calculateIncomeTax from our composable.
So what is happening here?
From our composable, we are importing the variables and the function we created. It’s as if we wrote those variables and the function within the component itself, as it will act in the same way.
When we click the button, we call the function calculateIncomeTax and insert the parameter income within the function. Remember how we declared income as a parameter in the composable?
function calculateIncomeTax(income) { let lowerTaxRate = 0.15; let higherTaxRate = 0.25; if (income < 40000) { leftOverIncome.value = income - (income * lowerTaxRate); totalTax.value = income * lowerTaxRate; } else { leftOverIncome.value = income - (income * higherTaxRate); totalTax.value = income * higherTaxRate; } }
It’s as if this function is directly written into this component. But instead, we imported it.