December 4th, 2024, posted in for_founders
by Adelina
Vue is amazing. Seriously, Evan You and the community have done an amazing job with it and I've spent a good part of my career working with Vue now. It kind of has become my go-to framework over the years and I would absolutely recommend it to everyone.
Over on dev.to I already shared 5 tips to get beginners with Vue.js started faster and I would like to share 10 more tips with you that I wish I had known when I started out with Vue.
Let’s jump right in, shall we?
1. Variable components give you more flexibility
There's a ton of CMS frameworks out there. Most of them can be used with a custom Vue frontend. Some of these frameworks offer configurable forms. Imagine you've got an endpoint that delivers the following JSON as a form definition:
[
{
"fieldLabel": "Topic",
"fieldType": "text"
},
{
"fieldLabel": "E-Mail address",
"fieldType": "email"
},
{
"fieldLabel": "You message",
"fieldType": "textarea"
}
]
You want to render this automcatically since the form could change any time and you don't want to adjust it every time the editors add or remove a field.
So, straight forward, you loop over the fields and add a bunch of v-ifs for the components, right? Let's see:
<template>
<div>
<div v-for="formField in form" :key="formField.fieldLabel">
<MyTextInput v-if="formField.fieldType === 'text'" :label="formField.fieldLabel" />
<MyEmailInput v-if="formField.fieldType === 'email'" :label="formField.fieldLabel" />
<MyTextArea v-if="formField.fieldType === 'textarea'" :label="formField.fieldLabel" />
<MyNumberInput v-if="formField.fieldType === 'number'" :label="formField.fieldLabel" />
<MyColorInput v-if="formField.fieldType === 'color'" :label="formField.fieldLabel" />
<!-- and so on -->
</div>
</div>
</template>
<script>
import MyTextInput from './MyTextInput.vue'
import MyEmailInput from './MyEmailInput.vue'
import MyTextArea from './MyTextArea.vue'
import MyNumberInput from './MyNumberInput.vue'
import MyColorInput from './MyColorInput.vue'
export default {
components: {
MyTextInput,
MyEmailInput,
MyTextArea,
MyNumberInput,
MyColorInput,
},
data() {
return {
form: [ /* ... */ ]
}
}
}
</script>
The template looks really cluttered. There's a lot of repetition as well and it's not really maintainable either. Instead, we can use a variable component:
<template>
<div>
<div v-for="formField in form" :key="formField.fieldLabel">
<component
:is="getFormFieldComponent(formField.fieldType)"
:label="formField.fieldLabel"
/>
</div>
</div>
</template>
<script>
import MyTextInput from './MyTextInput.vue'
import MyEmailInput from './MyEmailInput.vue'
import MyTextArea from './MyTextArea.vue'
import MyNumberInput from './MyNumberInput.vue'
import MyColorInput from './MyColorInput.vue'
export default {
components: {
MyTextInput,
MyEmailInput,
MyTextArea,
MyNumberInput,
MyColorInput,
},
data() {
return {
form: [ /* ... */ ],
}
},
methods: {
getFormFieldComponent(type) {
switch (type) {
case 'text':
return MyTextInput
case 'email':
return MyEmailInput
case 'textarea':
return MyTextArea
case 'number':
return MyNumberInput
case 'color':
return MyColorInput
default:
return MyTextInput
}
}
}
}
</script>
This has several advantages:
- The template has less logic
- A switch/case is more readable
- The entire logic can be put into a mixin, making it resuable
- It's unit testable without having to introduce the entire rendering chain
2. Event modifiers spare you some extra lines of code
There's certain events in JS you probably want to handle yourself and do not want the default behaviour to happen. The submit event is a classic one in that regard. Let's say you've got a form and want to submit this form via XHR. You've probably got a method for that on your form component. Since the semantic HTML you're using (<form> and <button type="submit">) have some default behaviour, you need to prevent the standard form submission in order to handle it via XHR instead. You could use the trusty ol' event.preventDefault() in the method - or you add an event modifier:
<template>
<form @submit.prevent="...">
<!-- ... -->
</form>
</template>
There's a ton of event modifiers for all kinds of things, even for listening to specific keyboard input!
3. Use watchers and debounce to auto-commit to Vuex
Let's say you've got a Vuex store that keeps track of some user input throughout the entire app. Perhaps it's their display name or some draft of a post you don't want to send to the server just yet. Ideally, though, the draft is comitted to the Vuex store every now and again. To not mess around with events too much, you can use the NPM package debounce and Vue's watchers for that:
<template>
<div>
<textarea v-model="userText" />
</div>
</template>
<script>
export default {
data() {
return {
userText: "...",
}
},
watch: {
userText() {
debounce(
this.saveTextInStore.bind(this),
1000
)
}
},
methods: {
saveTextInStore() {
this.$store.commit('saveText', this.userText)
}
}
}
</script>
This will wait for 1 second after the last key stroke to actually commit to the Vuex store, saving you some events and not polluting the store's histrory with them.
4. How to set computed properties
Computed properties are very handy. They let you calculate things in a reactive way and update automatically once one of the used fields is updated. Now, without additional syntax, computed properties are usually read only. They can be set, though!
Imagine the following: You've got a CRM app with a list of prices for two products and some additional shipping cost. Your setup might look something like this:
<template>
<div>
<input type="number" v-model="priceA">
<input type="number" v-model="priceB">
<input type="number" v-model="additionalShipping">
{{ total }}
</div>
</template>
<script>
export default {
data() {
return {
priceA: 120,
priceB: 130,
additionalShipping: 140,
}
},
computed: {
total() {
return this.priceA + this.priceB + this.additionalShipping
}
}
}
</script>
Pretty straight forward, right? Now what if you wanted to edit the total as well? Perhaps one of your users has made a special deal with one of their clients to not charge more than 300 in total. And they don't want to do the math themselves.
With setting the computed property, you can:
<template>
<div>
<input type="number" v-model="priceA">
<input type="number" v-model="priceB">
<input type="number" v-model="additionalShipping">
<!--{{ total }}-->
<input type="number" v-model="total">
</div>
</template>
<script>
export default {
data() {
return {
priceA: 120,
priceB: 130,
additionalShipping: 140,
}
},
computed: {
// total() {
// return this.priceA + this.priceB + this.additionalShipping
// }
total: {
get() {
return this.priceA + this.priceB + this.additionalShipping
},
set(newValue) {
this.additionalShipping = this.additionalShipping + (newValue - this.total)
}
}
}
}
</script>
Now, each time total is updated, the difference will be added/deducted from the additionalShipping. And the neat thing: the computed property can now be used with v-model as well!
5. Components don't need templates
Components with templates and perhaps even without the <script> tag are most often used in Vue apps. But sometimes you simply don't need a template at all, only some functionality.
Image you've got a large form. Most of the fields offer some kind of custom validation with custom rules. The validation is triggered as an event to the parent, probably the look and feel of the entire form. While you could use a mixin for this, you can also create a template-less BaseFormField.vue component:
<!-- BaseFormField.vue -->
<script>
export default {
data() {
return {
value: '...',
validationRules: [
// ...
],
}
},
watch: {
value() {
this.validate()
}
},
methods: {
validate() {
// ...
this.$emit('validated')
}
}
}
</script>
This component can then be extended by all your form fields:
<!-- MyTextInput.vue -->
<template>
<div>
<!-- ... -->
<input type="text" v-model="value">
<!-- ... -->
</div>
</template>
<script>
import BaseFormField from './BaseFormField.vue'
export default {
extends: BaseFormField
}
</script>
And now all your validation happens in one place! You can also easily overwrite and extend the validation rules (for example) by adding more via data.
6. You can call any child components methods - even though you probably shouldn't
Large Vue apps - just like any other app, really - can get messy, especially if you don't refactor things from time to time. Let's say you've got a feed component. This feed has a method load() to load all the feed items and displays them in a list:
<template>
<ul>
<feed-item v-for="item in feedItems" :item="item" :key="item.uuid" />
</ul>
</template>
<script>
export default {
data() {
return {
feedItems: [],
}
},
methods: {
loadFeedItems() {
// ...
}
},
mounted() {
this.loadFeedItems()
}
}
</script>
Now the client called - they urgently need a "reload feed" button in the navigation of the page. The navigation is located in the index.vue component and the feed component has no way of knowing about that button. So, how do we trigger loadFeedItems() from outside the feed?
Refs can help us here. By defining a ref, if that ref is a Vue component, you get access to all its methods, too:
<template>
<main>
<nav>
<!-- ... -->
<button type="button" @click="$refs.feed.loadFeedItems()">
Reload
</button>
</nav>
<feed ref="feed" />
</main>
</template>
Be aware, though, that this is a bad practice and should be refactored ASAP! It makes things intransparent and less maintainable.
7. Scope your styles to reduce side effects
Styled components are really handy. Everything you need to know about a part of the app - how it's defined, how it behaves and how it looks - are in one place. You can even go as far as defining styles for often used HTML tags in your main layout or app component, for example for headings:
<style>
h1 {
font-size: 2rem;
color: #2e2e2e;
margin-top: 2rem;
margin-bottom: 1rem;
}
</style>
But imagine you've got a component that adds a little bit of styling to its own h1 and those styles should appear nowhere else. That's a CSS class, right? But the CSS class is then also reusable again and nothing stops anyone from resuing that class in some other component, making it hard to track down where styling is actually coming from. This doesn't happen with scoped styling. If a style should only ever apply to this and only this component, you can add the scoped attribute to the style tag, like so:
<style scoped>
h1 {
font-size: 2.5rem;
}
</style>
No need for extra CSS classes!
8. There's probably a library for that already
A custom drag&drop component or file drop zone or you-name-it complex kind of thing sounds amazing to build. It's complicated, yes, but once it's up and running, the feeling of achievement is great! You'll love it and others will admire the effort.
But let's face it: Your end users probably won't. Chances are you haven't tested it on the old smartphone someone across the country uses to access your app, you might've forgotten about screen readers or keyboard input and all of a sudden you find yourself in a mess of unmaintainable event handlers and unexpected bugs and suddenly the thing you were once so proud of makes up 75% of your code base.
You can spare yourself a lot of the headache and wasted time by abandoning the "Not invented here" mindset and look for an OSS library instead. Chances are there's a library that's well maintained and tested that does exactly what you want it to do.
If you don't know where to start, have a look at awesome-vue.js.org or madewithvuejs.com - they've got components galore.
9. Use the Vue dev tools - especially for Vuex
Our trusty old debugger and console.log help to figure out how data flows and where something might be going wrong, but, especially when it comes to larger frameworks like Vue, chances are you'll lose the overview pretty quickly. There's a lot going on, after all.
Luckily, there's the VueJS devtools for both Firefox and Chrome! They can show you the component tree of your app, let you inspect the state of computed properties, even alter props and data on the fly! But the most amazing part of the devtools is the Vuex inspector: A list of every single event that has ever happend in your session together with its state and - most importantly - the ability to jump around between states! If there's a lot going on in your app, you can easily jump back to a state in the past and see exactly what the app looked like back then.
10. Use an event bus to pass events to unrelated components
Remember the reload button we introduced in tip #6? Bonkers - the client called again. It should now also reload the notifications, the amount of users currently online and while we're at it, make it reload the weather widget and everyone's avatar, too.
Calling methods of child components isn't gonna cut it anymore, sadly. Passing around events for something like this sounds like a mess, too: We would probably need to touch every component we have...
Let's use an event bus instead. We can create one by creating a Vue instance we share across multiple components:
// eventBus.js
import Vue from 'vue'
const eventBus = new Vue()
export default eventBus
We can now import this anywhere we need to emit events:
<!-- ReloadButton.vue -->
<template>
<div>
<button @click="reloadAll">
Reload
</button>
</div>
</template>
<script>
import eventBus from './eventBus.js'
export default {
methods: {
reloadAll() {
eventBus.$emit('reload')
}
}
}
</script>
And we can use it to listen to events:
<!-- Some other component -->
<script>
import eventBus from './eventBus.js'
export default {
methods: {
mounted() {
eventBus.$on('reload', () => {
// Reload weather, feed, avatar...
})
},
beforeDestroy() {
eventBus.$off('reload')
}
}
}
</script>
Sadly, this technique will be gone with Vue 3, but luckily, the official Vue docs have some handy migration guide for that.
And that's it for today! What's your most valuable tip for beginners? Leave a comment!