It is the entry point.
It takes 2 parameters:
- a list of props
- a context object
You can define the props above the setup method in the same way we do it in the Options API. But later in this article, we review the defineProps
way with TypeScript.
Inside the setup method, we accuess the props using the first parameter:
props: {
name: String,
price: Number
},
setup(props){
console.log(props.name);
console.log(props.price);
}
We can destructure the context to extract attr
, slots
and emits
for example.
setup(props, {attrs, slots, emit}){
/* your code here */
}
We can use whatever we want from the context. For example, we can use emit
only.
Primitives are string, integers, boolean.
A variable in the setup method is not reactive by default.
Using ref
, you can tell Vue it is.
import { ref } from "vue";
export default {
setup() {
const name = ref("The Snazzy Burger");
return { name };
},
};
Without ref
, you could not edit name
in Vue DevTools and have it update in the UI.
ref
is a reactive reference.
Using a ref
variable, you will need to use .value
to read or modify it within a setup
method.
In the template or lifecycle hooks like created()
where Vue unwrap the ref
.
IMPORTANT: the reason why you should ALWAYS declare variable in setup
as const
is to prevent breaking the reactivity by using let
and assigning a non-ref value to your initially declared ref
variable.
Non-primitives are objects and arrays.
You can use ref
, but you also have reactive
.
import { ref, reactive } from "vue";
export default {
setup() {
const appName = ref("The Snazzy Burger");
const meal = reactive({ name: "Hamburger", price: 5 });
return { appName, meal };
},
};
One advantage of reactive
is that it elimates the need to use .value
.
See the docs for more cool stuff about reactivity.
However, one of the trap of reactive
is that you may assign the variable a complete other value and you will loose the reactivity.
Using ref
guarantee it doesn't happen.
See the lesson "Refs vs Reactive With the Vue 3 Composition API" for the advantages and disavantages of each.
I personnally use exclusively ref
.
It is very similar to the Options API
import { computed } from "vue";
export default {
props: {
price: Number
},
setup(props,){
const prettyPrice = computed(() => `$${props.price.toFixed(2)}`);
return {
prettyPrice,
}
}
But when using composables, it will provide a new advantage.
ALWAYS copy the reactive array, otherwise, you will work on a reference, not a true copy.
//newCart and oldCart point to the same reference
watch(cart, (newCart, oldCart) => console.log(newCart, oldCart));
watch(
//create a copy of the reactive array
() => [...cart],
//newCart amd oldCart point to different references
(newCart, oldCart) => console.log(newCart, oldCart)
);
This is true for ref
or reactive
.
watchEffect
:
- fires immediatly
- doesn't need to be told what it depends on by passing the data as first attribut
- doesn't have an old value
For example, with watch
, we write:
import { watch } from "vue";
export default {
setup() {
const hideCartOnAddItem = watch(
() => [...cart],
(newCart, oldCart) => alert(newCart.join("\n"))
);
},
};
With watchEffect
, it becomes:
import { watchEffect } from "vue";
export default {
setup() {
const hideCartOnAddItem = watchEffect(() => alert(cart.join("\n")));
},
};
So when to use either one? Ask yourself:
- Do I need access to the old value?
- Will it be a problem if the callback fires inmediatly?
What about passing variables from component N to component N+2 without using a prop on component N+1
Use the methods provide
and inject
.
provide
defines in the component N the name and value of the variable.
inject
reads from component N+2 or deeper the variable's value defined in provide
by component N.
A provided variable can be made reactive.
Note: you cannot inject a variable not provided by a parent. However, inject
takes a second argument to prevent broken code.
const currencySymbol = inject("currencySymbol", ref("$"));
PS: using TypeScript, you can create an enum to list the names of those types of variables to avoid mistyping the variable name in the inject
method.
It is as simple as extracting the hook on the import, on as-you-need basis.
See the docs for the list of methods supported.
beforeCreate
and created
are exception because setup
is called around those two.
It is implicity return all variables and methods to become useable in the template.
One caviat about the setup
attribut: only single file component can use it.
To do so, use the spread operator:
const props = defineProps({
post: Object,
});
const post = { ...props.post };
A composable is a file, external to the components, that you can use in any component.
The convention is to:
- put the composables in a dedicated folder
composables
in thesrc
directory. - name the composable with the prefix
use
which is a convention in VueJs community to let people know it is a composable.
For example:
import { ref } from "vue";
export default function usePost() {
const posts = ref([]);
const fetchAll = async () => {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
posts.value = await response.json();
};
return {
posts,
fetchAll,
};
}
//fetchUser is dependant on the fetchPost, so we wait it resolves.
fetchPost(route.params.id).then(() => {
fetchUser(post.value.userId);
});
//fetchUser is dependant on the fetchPost, so we wait it resolves.
const post = (ref < Post) | (null > null);
const user = (ref < User) | (null > null);
const fetData = async () => {
post.value = await fetchPost(route.params.id);
user.value = await fetchUser(post.value.userId);
};
(async () => {
await fetchPost(route.params.id);
fetchUser(post.value.userId);
})(); //the final () is the call of the function.
To keep the logical concerns together, you can use a watch
:
const { item: post, fetchOne: fetchPost } = useResource("posts");
const route = useRoute();
fetchPost(route.params.id);
const { item: user, fetchOne: fetchUser } = useResource("users");
watch(
//when the post changes...
() => ({ ...post.value }),
//call the user api
() => {
fetchUser(post.value.userId);
}
);
IMPORTANT: on January 9th 2024, it is still under an experimental status. See the docs. However, many projects use it in production (Nuxt 3 for example).
The usage is the following:
<!--
the async is just "sugar" syntax, not the same as the asnc keyword on a asynchronous function...
-->
<script setup async>
import { watch } from "vue";
import { useRoute } from "vue-router";
import useResource from '../composables/useResource';
const { item: post, fetchOne: fetchPost } = useResource('posts');
const route = useRoute();
//...mark the function to wait on...
await fetchPost(route.params.id);
const { item: user, fetchOne: fetchUser } = useResource('users');
//...and fetchUser is same to call now since fetchPost is awaited.
fetchUser(post.value.userId)
</script>
To work, you will need to use the suspense
component higher up in the components tree, e.g. App.vue
:
<!-- in the template of App.vue -->
<suspense>
<template #default>
<router-view></router-view>
</template>
<template #fallback>
<p>Loading...</p>
</template>
</suspense>
It is important to note that Both slots only allow for one immediate child node.
But with router-view
, it is more like this that you need to implement it:
<!-- in the template of App.vue -->
<router-view v-slot="{ Component }">
<template v-if="Component">
<suspense>
<template #default>
<component :is="Component"></component>
</template>
<template #fallback>
<p>Loading...</p>
</template>
</suspense>
</template>
</router-view>
Read more in the documentation.