The Composition API is a new way to organize and reuse code in Vue 3. It allows you to define your component's logic in a more modular and composable way, making it easier to reason about and maintain your code. To get the most out of the Composition API, it's important to embrace its principles and use it consistently throughout your application.
One of the key benefits of the Composition API is that it allows you to separate your component's logic from its template. This makes it easier to test and reuse your code, as you can write unit tests for your logic without having to worry about the template. It also makes it easier to share code between components, as you can extract common logic into reusable functions and hooks.
Here we define a composable useCounter (it's a convention to prefix your composable name by "use") which could be seen as a store for a simple reactive counter property :
import { ref } from "vue";
function useCounter() {
const counter = ref(0);
function increment() {
counter.value++;
}
function decrement() {
counter.value--;
}
return {
counter,
increment,
decrement,
};
}
export default useCounter;
The Script Setup syntax is a new feature in Vue 3 that makes it easier to use the Composition API. It allows you to define your component's logic using a more concise and readable syntax. This can help reduce boilerplate code and make your components easier to understand as it looks like plain javascript.
Notice how we used to handle the script setup, exporting a setup function which handles the component's logic before returning every value that is supposed to be used in the template.
<template>
<div>
<p>{{ message }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script>
import useCounter from "./useCounter";
export default {
name: "Greeting",
setup() {
const { counter, increment, decrement } = useCounter();
const message = `The count is ${counter.value}`;
return {
message,
increment,
decrement,
};
},
};
</script>
With the script setup sugar syntax all we have to do is care about the component's logic the rest will be taken care of during compilation by VueJs!
<template>
<div>
<p>{{ message }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script setup>
import useCounter from "./useCounter";
const { counter, increment, decrement } = useCounter();
const message = `The count is ${counter.value}`;
</script>
Reactivity is a core concept in Vue 3, and it's essential for building dynamic and responsive applications. The Composition API provides two main ways to create reactive data: Reactive and Ref:
ref, it returns an object with a value property that holds the actual value. You can read and write to this value property to access or modify the value, and Vue will automatically track any changes and update the template as necessary.As a general rule of thumb, it is recommended to use ref for primitive values and reactive for everything else. Here let's say you want to define a simple counter:
import { ref } from "vue";
const count = ref(0); // create a reactive reference to the number 0
count.value++; // count = 1
One important thing to note is that when you modify a property of a reactive object, you need to do so using the property's name, rather than a value property like you would with a ref. This is because reactive objects are converted into getters and setters, so when you access or modify a property, you are actually calling the getter or setter function that Vue has created for that property.
import { reactive } from "vue";
const state = reactive({ message: "Hello", likes: 0 }); // create a reactive object with two properties
state.message = "Hi"; // modify the message property
state.count++; // increment the count property
In Vue 3 as well as in Vue 2, v-model can be used on a component or an input to implement a two-way binding. When used on an input, v-model expands to a value attribute and an input binding depending on the type of input. Let's see into what v-model compiles:
<input v-model="search" />
<!-- compiles to: -->
<input :value="search" @input="search = $event.target.value" />
When used on a component, v-model compiles to a modelValue prop and an update:modelValue custom event:
<my-component v-model="search" />
<!-- compiles to: -->
<my-component
:modelValue="search"
@update:modelValue="newValue => search = newValue"
/>
Which allows you to play around with the side effect when needed either from the parent:
<MyComponent
:modelValue="search"
@update:modelValue="(newValue) => differentSideEffect"
/>
From the child:
function updateHandler(search) {
const search_trimmed = search.trim();
emit("update:modelValue", search_trimmed);
}
Another way to use v-model is to implement v-model on a component by using a writable computed property with both a getter and a setter. The get method should return the modelValue property and the set method should emit the corresponding event. By default, v-model on a component uses modelValue as the prop and update:modelValue as the event.
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit("update:modelValue", value);
},
}); // Usage
<MyComponent v-model="search" />
// compiles to:
<MyComponent :modelValue="search" @update:modelValue="search = $event" />
You can also target specific values by passing an argument to v-model. For example, in the template, we can use <MyComponent v-model:title="bookTitle" />
Computed properties and watchers are two powerful features in Vue 3 that allow you to create reactive logic that depends on other reactive data, let's see how we should use each:
Let's start with computed properties which ad their name suggests are functions that return a computed value based on one or more reactive dependencies.
They come in handy for creating derived data that depends on other reactive data. For example, you might use a computed property to calculate the total price of a shopping cart based on the prices and quantities of its items.
import { computed } from "vue";
const totalPrice = computed(() => {
return cartItems.value.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
});
Watchers on the other end are functions that are called whenever a reactive dependency changes. Watchers are useful for reacting to changes in reactive data. For example, you might use a watcher to update the search results based on a user query.
import { ref, reactive, watch } from "vue";
const search = ref("");
const searchResults = reactive([]);
watch(search, (newValue, oldValue) => {
console.log(`Search term changed from ${oldValue} to ${newValue}`);
// perform some action when search changes before updating the searchResults
});
Note: that if you have props and you want to watch one of them you need to provide the props as a getter
import { watch, defineProps } from "vue";
const props = defineProps({
selectedItem: Object,
});
watch(
() => props.selectedItem,
(newValue, oldValue) => {
console.log(`selectedItem prop changed from ${oldValue} to ${newValue}`);
}
// perform whatever side effect you planned to here
);
Vue 3 offers a powerful event system that allows components to communicate with one another. Components can emit custom events directly in template expressions using the built-in $emit method.
For example, we can emit a custom event called buttonClicked when a button is clicked: <button @click="$emit('buttonClicked')">click me</button> This will emit the buttonClicked event, which the parent component can listen to using v-on: <MyComponent @button-clicked="callback" />
Note: Event names in Vue 3 provide automatic case transformation. This means that you can emit a camelCase event and listen for it using a kebab-case listener in the parent. For consistency, it's recommended to use kebab-case event listeners in templates.
Sometimes, it's useful to emit a specific value with an event. To do so, we can pass extra arguments to $emit: <button @click="$emit('increaseBy', 1)"> Increase by 1 </button> When the parent component listens to the increaseBy event, it can use an inline arrow function to access the event argument:
<template>
<MyButton @increase-by="(n) => (count += n)" />
<!-- Or use a method -->
<MyButton @increase-by="increaseCount" />
</template>
<script setup>
function increaseCount(n) {
count.value += n;
}
</script>
Don't forget to define and document emitted events to better document how a component should work.
Note: This also allows Vue to exclude known listeners from fallthrough attributes, avoiding edge cases caused by DOM events manually dispatched by third-party code.
<script setup>
const emit = defineEmits(["submit"]);
function buttonClick() {
emit("submit");
}
</script>
Similar to prop type validation, an emitted event can be validated if it is defined with the object syntax instead of the array syntax. T
o add validation, the event is assigned a function that receives the arguments passed to the $emit call which allows us to perform runtime validation of the payload of the emitted events:
<script setup>
const emit = defineEmits({
submit(payload);
// return `true` or `false` to indicate // validation pass / fail
});
</script>
Another feature that make Vue 3 so great is the slot system. Slots allow you to pass template fragments to child components and have them render within their own template. Slots are a powerful feature that allows for great flexibility and reusability in your Vue 3 components.
To understand slots, let's start with an example. Suppose you have a component called CustomButton that renders a styled card. You want the card content to be dynamic and determined by the parent component. You can achieve this with slots.
The template for CustomButton might look like this:
<!-- From child -->
<template>
<button class="custom-btn"
<slot></slot>
</button>
</template>
<!-- From parent -->
<template>
<CustomButton>Click me!</CustomButton>
</template>
With slots, the CustomButton component is responsible for rendering the outer button (and its custom styling), while the inner content is provided by the parent component.
The slot element is a slot outlet that indicates where the parent-provided slot content should be rendered. The final rendered DOM will look something like this:
<button class="custom-btn">Click me!</button>
If you wanna go deeper into the customability of your componentit may be useful to have multiple slot outlets in a single component. For example, in a CustomCard component, we may have a title, description, and image slot.
In order to accomplish this we need to use multiple slots where each slot as a unique ID assigned so that the compiler can determine where content should be rendered. The <slot> element has a special attribute called name right for this purpose. Otherwise the default slot will just be rendered multiple times.
<template>
<!-- Bad -->
<div class="card">
<h2>
<slot></slot>
</h2>
</div>
<span>
<slot></slot>
</span>
<div class="img">
<slot></slot>
</div>
<!-- Good -->
<div class="card">
<h2>
<slot name="title"></slot>
</h2>
<span>
<slot name="description">
<!-- this acts as a fallback value -->
My card description
</slot>
</span>
<div class="img">
<slot name="image"></slot>
</div>
</div>
<!-- Good -->
<CustomCard>
<template v-slot:title>
<!-- content for the title slot -->
</template>
<template v-slot: description>
<!-- content for the description slot -->
</template>
<template v-slot:image>
<!-- content for the image slot -->
</template>
</CustomCard>
<!-- Even better -->
<BaseLayout>
<template #title>
<!-- content for the title slot -->
</template>
<template #description>
<!-- content for the description slot -->
</template>
<template #image>
<!-- content for the image slot -->
</template>
</BaseLayout>
</template>
Note: I'd advise you to name each slot in case of multiple slots even for the default slots at it's not obvious for everyone that a <slot> outlet without name implicitly has the name "default".
One last thing to talk about is passing data to slots, by default slot content does not have access to the child component's data. Expressions in Vue templates can only access the scope it is defined in, consistently with JavaScript's lexical scoping.
There are several ways to pass values to slots in Vue 3, depending on the specific use case:
v-bind directive. Here's an example:<!-- Parent Component -->
<template>
<my-component>
<template #my-slot="{ text }">
{{ text }}
</template>
</my-component>
</template>
<!-- Child Component -->
<template>
<div>
<slot name="my-slot" :text="message"></slot>
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello World",
};
},
};
</script>
In this example, we're passing the value of the message data property from the child component to the parent component using a prop. We're then binding the value of the text prop to the slot using the v-bind directive, and rendering it within the slot content using the {{ text }} template syntax.
v-on directive. Here's an example:<!-- Parent Component -->
<template>
<my-component>
<template #my-slot="{ handleClick }">
<button @click="handleClick">Click Me</button>
</template>
</my-component>
</template>
<!-- Child Component -->
<template>
<div>
<slot name="my-slot" :handle-click="handleClick"></slot>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
console.log("Button Clicked");
},
},
};
</script>
In this example, we're passing a handleClick method from the child component to the parent component using a function. We're then binding the value of the handleClick method to the slot using the v-on directive, and using it as an event handler for a button within the slot content.
As a Vue.js developer, you will frequently encounter situations where you need to pass data between components especially when dealing with deeply nested components In such cases, using props can be challenging since you might need to pass the same prop across the entire chain, which is known as prop drilling.
To solve this issue, Vue.js provides the provide and inject functions that allow a parent component to provide data to all of its descendants, regardless of how deeply nested they are.
The provide function is used to provide data to a component's descendants. It accepts two arguments: the first is the injection key, which can be a string or a symbol and will be used to retrieve the value. The second argument is the value that you want to provide, which can be of any type, including reactive state such as refs.
<script setup>
import { provide } from "vue";
import { ref } from "vue";
const message = ref("Hello World!");
provide("message", message);
</script>
You can provide multiple values to a component by calling provide multiple times with different injection keys.
<script setup>
import { provide } from "vue";
provide("foo", "foo");
provide("bar", "bar");
provide("baz", "baz");
</script>
If you need to provide data at the app level, you can use app.provide instead.
import { createApp } from "vue";
const app = createApp({});
app.provide("message", "Hello World!");
On the other hand the inject function is used to inject data provided by an ancestor component. It accepts a unique argument, the injection key used previously as the first argument to the provide function.
<script setup>
import { inject } from "vue";
const message = inject("message");
</script>
If the provided value is a ref, it will be injected as-is and will not be automatically unwrapped. This allows the injector component to retain the reactivity connection to the provider component.
If the injected property is not provided by an ancestor component, you will see a runtime warning. To avoid this warning, you can provide a default value similar to props.
<script setup>
import { inject } from "vue";
const message = inject("message", "default value");
</script>
In some cases, the default value may need to be created by calling a function. To avoid unnecessary computation or side effects in case the optional value is not used, we can use a factory function for creating the default value.
<script setup>
import { inject } from "vue";
const message = inject("message", () => new ExpensiveClass());
</script>
When using reactive provide/inject values, it's recommended to keep any mutations inside of the provider whenever possible. This ensures that the provided state and its possible mutations are co-located in the same component, making it easier to maintain in the future.
If you need to update the data from an injector component, we recommend providing a function that is responsible for mutating the state.
<script setup>
import { provide, ref } from "vue";
const city = ref("Paris");
function updateLocation(newValue) {
city.value = newValue;
}
provide("location", { location, updateLocation });
</script>
<!-- in injector component -->
<script setup>
import { inject } from "vue";
const { location, updateLocation } = inject("location");
</script>
With the Composition API, Vue introduced the concept of composables, which are functions that encapsulate reusable stateful logic. As the name implies, a composable is a unit of composition that can be combined with other composables to create more complex logic.
Composables are similar to hooks in React or mixins in Vue 2, but they are more flexible and powerful. They can be used to encapsulate any kind of stateful logic, including event handling, data fetching, and animation.
To create a composable, you simply define a function that uses the Composition API to manage state and side effects. Here is an example of a composable that manages the app errors:
// error.js
import { reactive } from "vue";
export function useError() {
const error = reactive({
code: null,
message: null,
});
function setError({ code, message }) {
error.code = code;
error.message = message;
}
return { error };
}
Then to use the composable in a component, you simply need to import the function and call it. Here is an example of a component that uses the useError composable:
<script setup>
import { useError } from "./error.js";
const { error } = useError();
</script>
<template>
<div v-if="error">Error: {{ error }}</div>
</template>
One of the most powerful features of composables is their ability to be nested. This means that one composable can call another composable to create more complex logic. Here is an example of a composable that uses another composable:
// state.js
import { ref } from "vue";
import { useFetch } from "./fetch.js";
export function useState(url) {
const state = ref(null);
const { isLoading, error } = useFetch(url);
if (!isLoading && !error) {
state.value = processState();
}
function processState() {
// process data here
}
return { state, isLoading, error };
}
Here are some best practices for using composables in Vue 3:
Composables should start with the word "use" to indicate that they are a function that can be used as a composable. This convention makes it clear that the function uses the Composition API.
As with any function composables should be small and focused on a specific task. This makes them easier to understand, test, and reuse. If a composable is doing too much, consider breaking it down into smaller composables.
Composables should only expose the data that is necessary for other components to use them. This helps prevent components from depending on data they don't need, which can lead to unnecessary re-renders.
Composables should use reactive data, such as ref and reactive, to manage state. While their mutation should happen inside the composables and eventually exposing their mutation logic. This ensures that concerns are properly split and changes to the state trigger reactive updates in the component that uses the composable.
Composables should contain stateful logic and not presentation-related code. This helps keep components focused on rendering and ensures that logic is reusable across multiple components.
Composables should be tested in isolation from the components that use them. This ensures that the composable works as expected and is not dependent on any specific component implementation.
Vue 3 provides a number of lifecycle hooks that allow you to perform actions at specific points in a component's lifecycle. These hooks include created, mounted, updated, and destroyed:
Here's another example that demonstrates how the composition API can make it easier to handle lifecycle hooks:
Options API:
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello, world!",
};
},
created() {
console.log("Component created");
},
mounted() {
console.log("Component mounted");
},
beforeUnmount() {
console.log("Component about to be unmounted");
},
destroyed() {
console.log("Component destroyed");
},
};
</script>
Using the composition API:
<template>
<div>
<p>{{ message }}</p>
</div>
</template>
<script setup>
import { onMounted, onBeforeUnmount } from "vue";
// This is the equivalent of the created hook
const message = "Hello, world!";
onMounted(() => {
console.log("Component mounted");
});
onBeforeUnmount(() => {
console.log("Component about to be unmounted");
});
</script>
In the options API example, we define the lifecycle hooks using separate functions. In the composition API example, we use the onMounted and onBeforeUnmount functions to define the mounted and beforeUnmount hooks respectively. This makes it easier to manage the lifecycle hooks and also makes the code more readable.
Vue 3 is a powerful and flexible framework that provides a number of new features and improvements. By leveraging the Composition API and the Script Setup syntax, you can create more modular, composable and concise code that is easier to reason about and maintain. By using reactive data, computed properties, watchers, and lifecycle hooks, you can create dynamic and responsive applications that provide a great user experience. By following these best practices, you can get the most out of Vue 3 and build high-quality applications with ease.
Honestly i am no one, i've just been coding for 3 years now and i like to document every solutions to every problem i encounter. Extracting as much code snippets and tutorials i can so that if i ever need it again i just have to pop back here and get a quick refresher.
Feel free to me through this learning journey by providing any feedback and if you wanna support me: