DEV Community

Cover image for Vue 3 Best Practices, Write a Better Code
Adnan Babakan (he/him)
Adnan Babakan (he/him)

Posted on

Vue 3 Best Practices, Write a Better Code

Hey DEV.to community.

In this article, I will discuss how you should (or at least I believe so :)) organize your Vue 3 code. Although the main focus of this post is based on Vue 3, the principles mentioned here are applicable to Vue 2 and any other framework/library. Please correct me if I'm wrong or you know a better way.

Data initialization

You will get your hands dirty with states and data no matter the project. This topic gets harder to manage in certain cases especially when dealing with data being fetched from an external source such as a RESTFul API.

When initializing a ref you may pass its default value as you know. However, choosing the correct initial value might be difficult.

Imagine we want to store an array of products inside a ref. But the array will be fetched from the API and obviously, it needs some time or might fail.

What you might suggest is to set the default value of products as ref([]) which generates a ref with an empty array as its default value. The problem with this type of initialization is that you wouldn't know if the actual response of the API was an empty array (for instance no products found) or simply the request failed or something else happened. And for sure you need to have a loading ref to indicate the loading process and use it in your UI to show some loading animation or something.

So I suggest initializing the value of your products with ref(null). If you do so you may show if the request is loading or the request is finished and there are no results. This mode solves the collision of products being empty and requests failing while the initial value of an empty array is preserved.

<template>
    <div>
        <div v-if='!loading'>
            <div v-if='products'>
                <div v-if='products.length > 0'>
                    <!-- Products here -->
                </div>
                <div v-else>
                    <!-- No products found error here -->
                </div>
            </div>
            <div v-else>
                <!-- Something is actually wrong -->
            </div>
        </div>
        <div v-else>
            <!-- Loading animation here -->
        </div>
    </div>
</template>

<script setup>
const loading = ref(false)
const products = ref(null)

const init = async () => {
    loading.value = true
    try {
        products.value = await getProducts()
    } catch (e) {
    }
    loading.value = false
}

onMounted(() => {
    init()
})
</script>
Enter fullscreen mode Exit fullscreen mode

Using lifecycle hooks

Lifecycle hooks are the basic fundamental of any Vue app and give you the power to control what happens in certain steps of the app. The thing most developers tend to do is to write their logic inside the hooks itself which can bring many problems and can toughen debugging or reduce the reusability of the logic.

Imagine that you want to fetch some data from an API, make some modifications to the result and assign the result to a ref of your choosing. Just like the example above in which we fetched the products and assigned them to the products ref when the component was mounted.

The first way you might think of doing so would highly likely look like this:

<script setup>
...
onMounted(async () => {
    loading.value = true
    try {
        products.value = await getProducts()
    } catch (e) {
    }
    loading.value = false
})
...
</script>
Enter fullscreen mode Exit fullscreen mode

This code is absolutely correct and works perfectly. But given that you need to add a refresh button to your app and make your users able to refetch the data you will need to repeat the code inside the onMounted hook and your code would look like this:

<template>
    ....
    <button @click="refreshData">Refresh</button>
    ....
</template>

<script setup>
...
onMounted(async () => {
    loading.value = true
    try {
        products.value = await getProducts()
    } catch (e) {
    }
    loading.value = false
})

const refreshData = async () => {
    loading.value = true
    try {
        products.value = await getProducts()
    } catch (e) {
    }
    loading.value = false
} 
...
</script>
Enter fullscreen mode Exit fullscreen mode

Such code is not easy to maintain since if you wanted to make a small change to the procedure of fetching and assigning the data you will need to do it more than once (twice in this case).

A better solution is to wrap your fetch/assign logic inside another function and call it wherever you need:

<template>
    ....
    <button @click="getProductsHandler">Refresh</button>
    ....
</template>

<script setup>
...
onMounted(async () => {
    getProductsHandler()
})

const getProductsHandler = async () => {
    loading.value = true
    try {
        products.value = await getProducts()
    } catch (e) {
    }
    loading.value = false
} 
...
</script>
Enter fullscreen mode Exit fullscreen mode

Now you only have one base for your logic and it is way simpler, cleaner and easier to make changes to.

Computed values and side effects

Well, this one is super important and can save you a lot of time when you don't know what is happening inside your app and you are losing your mind over it.

Some JavaScript methods/functions have side effects, meaning that they mutate (a fancy word for changing :)) the original value as well. For instance, reverse() method of arrays in JS not only returns the reverse of an array, but it actually reverses the original array as well.

To test this scenario you can run the code below in any JS environment:

const arr = [1, 2, 3, 4, 5]
arr.reverse()
console.log(arr) // [5, 4, 3, 2, 1]
Enter fullscreen mode Exit fullscreen mode

Now imagine you reach the same approach inside of a computed value in Vue:

<script setup>
const arr = ref([1, 2, 3, 4, 5])
const reverseOfArr = computed(() => arr.value.reverse())
</script>
Enter fullscreen mode Exit fullscreen mode

This kind of method can ruin your app and result in unexpected behaviours.

As seen in the JS code, although this computed value returns the reverse of the array, it mutates the original value as well, so each time the value of arr is changed the computed value is going to be recalculated thus changing your original value as well.

Avoid using such methods inside computed values. Instead of the computed value above you better write it yourself in an imperative way to make sure the behaviour you get is the one you want:

<script setup>
const arr = ref([1, 2, 3, 4, 5])
const reverseOfArr = computed(() => {
    const t = []
    for(let i = arr.value.length - 1; i >= 0; i--) {
        t.push(arr.value[i])
    }
    return t
})
</script>
Enter fullscreen mode Exit fullscreen mode

Mutating props

It is always suggested against mutating the props directly. Props are the values you receive from the parent component (the component where the current component is called from).

Imagine we want to have a simple number input component with two simple increase and decrease buttons. Here is how to implement it:

<template>
    <div>
     <button @click='decrease'>-</button>
     <div>{{ v }}</div>
     <button @click='increase'>+</button>
    </div>
</template>

<script setup>
const props = defineProps({
    modelValue: {
        type: Number,
        required: true
    }
})

const emits = defineEmits(['udpate:modelValue'])

const v = ref(props.modelValue)

const increase = () => {
    v.value++
}

const decrease = () => {
    v.value--
}

watch(v, () => {
    emits('udpate:modelValue', v.value)
})
</script>
Enter fullscreen mode Exit fullscreen mode

As you can see, it is better to assign the value of modelValue (which is a special prop that is attached to v-model when the component is used) to a secondary ref and manipulate its data instead of props.modelValue. Then by watching the value of v we may use an emit called update:modelValue to inform the parent component about the recent changes to the value.


BTW! Check out my free Node.js Essentials E-book here:

Feel free to contact me if you have any questions or suggestions.

Top comments (0)