The following are a kind of a checklist.
Why? What does the :key
attribut do?
While it will be OK not to put a :key
for static elements (e.g. elements that don't contain their own state like p
, li
, span
, label
), the ones that do contain the own state will not behave properly.
For example, input
or textarea
elements or even components with their own state will produce a weird result.
See the demo of the issue with a key provided.
Also, if you're using the Vue Official extension and ESLint extension in Visual Studio Code, you will see an error or a warning, depending on your settings.
ESLint will show you the issue.
However, it doesn't stop there: having a key that contains a same value for multiple entries in an array will cause the same effect.
So what should and shouldn't the key be?
- not the index in the array
- not the same value of each value
So how do you create unique key?
With a list of objects, you could concatenate 2 properties or JSON.stringify
the object.
Read more on this blog post.
It is defined by passing a prop down several level from a component to its N+2
child or more.
The same is true when component N+2
emits back...
It is anti-pattern.
How to avoid it?
See the docs for more details or my notes on the course "Composition API".
That is how VueRouter does it.
A global state doesn't only mean Vuex or Pinia, but a simple composable can work too for simplier use cases. You will then need to use the composable on each component.
The caveat: set the data of the composable outside of the composable. See the example.
Otherwise, the data is initialized every time you call the composable.
Read the docs for more details.
Watching arrays requires to use the deep mode.
watch(numbers, () => console.log("new number added"), {
//deep option
deep: true,
});
Why?
When you add, remove or modify an item in an array, you must use the deep
option so that Vue is working against a brand new array to spot the difference.
Another syntax is to use an anonymous arrow function:
watch(
() => [...numbers.value],
() => console.log("new number added")
);
The same applies to objects!
We cannot replace a whole reactive
object, NEVER.
Use ref
instead when you replace a reactive variable entirely.
Also, remember: replacing entirely a reactive
variable's value will break the reactivity...
const myData = reactive({ name: "John Doe", age: 36 });
//❌ NEVER DO THAT
myData = { name: "Jane Doe", age: 34 };
It seems that using reactive
may depreceated someday... or use it wisely! I personally don't use it because I am not bothered by the .value
you need to use in the script setup.
const myData = ref({ name: "John Doe", age: 36 });
// ✅ Reactivity remains enabled
myData.value = { name: "Jane Doe", age: 34 };
Mutating props means modifying a property of an object prop passed by a component to a child component using v-model
in the child component.
The effect is that, if the child modifies a property of the object prop, the change reflects on the parent component.
Doing the same with a primitive prop would log a warning because props are readonly.
To avoid that, in the child component, create a local data variable with ref
of a copy of the object prop and bind, for example the inputs of a login form, to that local variable instead of the object prop.
For an object one-level-deep, use the spread operator ({...myObject}
).
//Using Composition API and TypeScript
const props = defineProps<{
user: User;
}>();
const editedUser = ref({ ...user.value });
Also, ESLint can save the day as, if configured correctly, it will show a hint that something is incorrect.
If you do use manual event listeners and register them on the onMounted
hook, ALWAYS unregister the listeners on onUnmounted
hook.
import { onMounted, onUnmounted } from "vue";
onMounted(() => {
document.body.addEventListener("keydown", handlerFunction);
});
onUnmounted(() => {
document.body.removeEventListener("keydown", handlerFunction);
});
If you want to avoid coding the both hook each time, in the package @vueuse/core
, you have a composable useEventListener
that makes sure the registration and unregistration are done in a single line:
useEventListener(document.body, "keydown", handlerFunction);
Computed relies on reactive dependencies in order to know when it should update.
If the dependency is not reactive, let's say localStorage
data, then the data is not reactive even if the variable was initialized from a computed.
FOr example:
<script setup lang="ts">
import { computed, ref } from 'vue'
const reactiveData = computed(() => localStorage.getItem('data))
function updateData(e: Event) {
const data = (e.target as HTMLInputElement).value
localStorage.setItem('data', data || '')
reactiveData.value = data
}
</script>
<template>
<div>
<input type="text" @input="updateData" :value="reactiveData" />
</div>
<pre>{{ reactiveData }}</pre>
</template>
reactiveData
won't show the input of the user because:
- the dependency of
reactiveData
isn't reactive. data
inupdatedData
isn't reactive.
Those non-reactive sources are typically browser native APIs: localStorage API, date, network events, DOM elements, clipboard, location API, etc...
The trick is to:
- store the initial value of the variable in a
ref
to make it reactive - make sure you update the variable's value when it needs to.
<script setup lang="ts">
import { computed, ref } from 'vue'
const reactiveData = ref(localStorage.getItem('data'))
function updateData(e: Event) {
const data = (e.target as HTMLInputElement).value
localStorage.setItem('data', data || '')
reactiveData.value = data
}
</script>
Otherwise, the library VueUse can help make those non-reactive sources reactive with little extra code. But learn it without it, as demonstrated above.
TypeScript will make the developper experience much better and smoother because silent issues will scream out loud.
See this example that contains an error:
<script setup>
import { ref } from "vue";
const user = ref({
username: "danielkelly_io",
email: "daniel@vueschool.io",
name: "Daniel Kelly",
});
</script>
<template>
<ul>
<li>
<strong>Username</strong>
<span>{{ user.username }}</span>
</li>
<li>
<strong>Email</strong>
<span>{{ user.mail }}</span>
</li>
<li>
<strong>Name</strong>
<span>{{ user.name }}</span>
</li>
</ul>
</template>
With vanilla JavaScript, nothing in the IDE tells you that line 18 is problematic.
Using TypeScript, ESLint would underline the mail
property of user
and tell you that it doesn't exist.
It may take a few month to learn, but the advantages far exceed the drawbacks.
Now, I came back to this paragraph a few months after and I want to share what I've learned from using TypeScript on the masterclass of Vueschool.io.
When you destructure an object, even if it is reactive, the destructured properties loose the reactivity.
The options are either to:
- use
computed
for readonly variable used in your template. - use the actual object's property.
- use
toRefs
fromvue
and wrap thereactive
variable with it before destructuring:
import { reactive, toRefs } from 'vue'
const myObject = reactive({
prop1: "Value1",
prop2: "Value2"
prop3: "Value3"
});
//prop1 to prop3 are not reactive because only the object is.
const { prop1, prop2, prop3 } = myObject;
//prop1 to prop3 are reactive because they are ref()s.
const { prop1, prop2, prop3 } = toRefs(myObject);
This can become an issue in a composable: call toRefs
on the returned object:
import { reactive, toRefs } from "vue";
export const useLion = () => {
const animal = reactive({
name: "Lion",
diet: "Carnivore",
lifespan: "8-12 years",
});
return toRefs(animal);
};
Composables should not be called from within in the body of a function, even if it is declared in a <script setup>...</script>
or any other script.
Composables should only be called from:
- in
<script setup>...</script>
or thesetup
method - in some lifecycle hooks.
- in the top level of a composable
Also, you should call a composable synchronously.
Why theses restrictions? It comes to the contexts where Vue is able to determine the current active component instance. That is necessary to register lifecycle hooks and to link the computed
and watch
to that active component.
I'll share an example:
<script setup lang="ts">
import { useA } from '@/composables/useA'
const a = useA()
const toAvoid =() {
//✅ This is OK
const a = useA();
}
const toPrefer =() {
//❌ This is wrong
a.doWhatever();
}
</script>
<template>
<div>Only use composables at the top level of script setup</div>
</template>
However, you can use a composable with another composable at the top level of the composable using the other one:
import { useB } from "./useB";
export const useA = () => {
//✅ This is OK
const b = useB();
function doThing() {}
//❌ This is wrong
function doSomethingElse() {
const b = useB();
}
};
Read more in the official docs.
Use v-html
:
- from a trusted source
- from sanitized data
Don't use v-html
if the source:
- comes from an
input
ortextarea
- is available to an external use.
Just don't manipulate the DOM when you use VueJS.
Period.
However, you will sometimes need access to native methods on a DOM element, like a dialog
element.
On a dialog
element, you want to use the showModal()
and close()
to handle the modal lifecycle.
Using template refs is the best practice to access the DOM elements.
See the usage in this example.
Write me a message with an example and I'll review it.
Thanks.