Get the most out of Vue
Successfully building ๐ a large Vue.js 3 App
Dima Vishnevetsky
๐ UX / UI Designer
๐ผ Media Developer Expert @ Cloudinary
๐จโ๐ซ #HackathonMentor
๐ Lecturer
๐ฃ International Tech speaker
๐จโ๐ป Front End Expert
Co-founder of
Vue.js Israel community leader
Performance
Page Load Time: There are many ways to improve the load time such as lazy-loading sections or infinite scrolling. Using a CDN (Content Delivery Network) to significantly reduce load time of assets and javascript files.
ย
Caching: If you are working on a public facing web application, chances are you will have to implement some type of caching solution so that you donโt have to re-render the page every time.
Server-side Rendering: This can be useful for high traffic public facing sites with content from a CMS, a cache can be used to store rendered pages.
Client-side Rendering: This can be useful for single page web applications where there is a lot of dynamic content and a complex front-end UX/UI.
Predictability
pinia.vuejs.org
vitejs.dev
vuetifyjs.com
Component files
Vue.component('TodoList', {
// ...
})
Vue.component('TodoItem', {
// ...
})
Bad
components/
|- TodoList.vue
|- TodoItem.vue
Best
Base components
components/
|- MyButton.vue
|- VueTable.vue
|- Icon.vue
Bad
components/
|- BaseButton.vue
|- BaseTable.vue
|- BaseIcon.vue
Best
Single instance components
components/
|- Heading.vue
|- MySidebar.vue
Bad
components/
|- TheHeading.vue
|- TheSidebar.vue
Best
Tightly coupled component names
components/
|- TodoList.vue
|- TodoItem.vue
|- TodoButton.vue
components/
|- TodoList/
|- Item/
|- index.vue
|- Button.vue
|- index.vue
Bad
components/
|- TodoList.vue
|- TodoListItem.vue
|- TodoListItemButton.vue
Best
Full-word component names
components/
|- SdSettings.vue
|- UProfOpts.vue
Bad
components/
|- StudentDashboardSettings.vue
|- UserProfileOptions.vue
Best
Typical CRUD application has the following different pages for each resource:
a list of all the resources
a view of a single resource
a form to create the resource
and a form to edit the resource
Path | Route and Component Name |
---|---|
/cars | CarsIndex |
/cars/add | CarsAdd |
/cars/{id} | carsShow |
/cars/{id}/edit | CarsEdit |
<router-link :to="{name: 'CarsIndex'}">Cars</router-link>
Help your tools to help you
ESLint, Prettier, Vite
Code Formatting with Prettier
Linting with ESLint
Vite project
Volar
*You need to disable Vetur to avoid conflicts.
ESLint docsย - Industry standard linting tool
Prettier docsย - Industry standard formatting tool
eslint-plugin-vueย - ESLint config that provides rules specific to Vue
eslint-config-prettier repoย - ESLint config to disable rules that would conflict with Prettier
Vetur- Legacy VS Code extension to provide syntax highlighting for .vue files and more
Volarย - VS Code extension to provide syntax highlighting for .vue files and more
Viteย - Super fast modern build tool
vite-plugin-eslint repoย - Vite plugin to show ESLint errors in the browser
Composables
// FetchPostMixin.js
export default {
data() {
return {
requestState: null,
post: null
};
},
computed: {
loading() {
// ...
},
error() {
// ...
}
},
methods: {
async fetchPost(id) {
// ...
}
}
};
// PostComponent.vue
<script>
import FetchPostMixin from "./FetchPostMixin";
export default {
mixins:[FetchPostMixin],
};
</script>
//FetchPostComposable.js
import { ref, computed } from "vue";
export const useFetchPost = () => {
const loading = computed(/*...*/);
const error = computed(/*...*/);
const post = ref(null);
const fetchPost = async (id) => {
// ...
};
return { post, loading, error, fetchPost };
};
// PostComponent.vue
<script>
import { useFetchPost } from "./FetchPostComposable";
export default {
setup() {
//clear source of data/methods
const {
loading: loadingPost, //can rename
error, fetchPost, post } = useFetchPost();
return { loadingPost, error, fetchPost, post };
},
};
</script>
Prefer Composables Over Mixins
Always Clone Objects Between Components
// CarForm.vue
<template>
<div>
<label>
Model <input type="text" v-model="car.model">
</label>
<label>
licence Number <input type="text" v-model="car.licenceNumber">
</label>
</div>
</template>
<script>
export default {
props:{
car: Object
}
}
</script>
// CarForm.vue
<template>
<input type="text" v-model="form.model">
<input type="text" v-model="form.licenceNumber">
</template>
<script>
export default {
//...
data(){
return {
// spreading an object effectively clones it's top level properties
// for objects with nested objects you'll need a more thorough solution
form: {...this.car}
}
},
}
</script>
Bad
Good
The same concept applies when passing your reactive objects into Vuex actions or anywhere else outside of your component instance.
Child component should have to emit an event when the data changes, and IF the Parent component wanted to listen to the event and update its data, if it wants.
Write Tests
Vue Testing Library
<template>
<div>
<p>Times clicked: {{ count }}</p>
<button @click="increment">increment</button>
</div>
</template>
<script>
export default {
data: () => ({
count: 0,
}),
methods: {
increment() {
this.count++
},
},
}
</script>
import {render, fireEvent} from '@testing-library/vue'
import Component from './Component.vue'
test('increments value on click', async () => {
// The render method returns a collection of utilities to query your component.
const {getByText} = render(Component)
// getByText returns the first matching node for the provided text, and
// throws an error if no elements match or if more than one match is found.
getByText('Times clicked: 0')
const button = getByText('increment')
// Dispatch a native click event to our button element.
await fireEvent.click(button)
await fireEvent.click(button)
getByText('Times clicked: 2')
})
Automate your test runs and have test failures prevent deploys (CI/CD)
Build SDK's
(software development kit)
Less need to pour over API documentation. Instead, browse through SDK methods via the IntelliSense feature of your IDE.
โ
Keeping your API URL structure irrelevant to your actual application makes updates to the actual API simpler. Yes, your SDK has to know the structure but it's probably limited to one or 2 places and not littered throughout your application codebase.
ย
Much less likely to make a typo. In fact, your IDE will probably type half of it for you.
ย
Ability to abstract concerns related to making requests to your API. For instance, checking the request status can be something as simple as this: if(post.isLoading){}
ย
More easily refactor the API integration when the REST API changes (for instance when an endpoint name or the authentication method changes)
axios('https://carsapi.com/cars/123')
cars.find(123)
ย instead of calling axios or fetch directly within your components, you can use your SDK
Wrap Third Party Libraries
๐
// Http.js
import axios from 'axios'
export default class Http{
async get(url){
const response = await axios.get(url);
return response.data;
}
// ...
}
// Http.js
export default class Http{
async get(url){
const response = await fetch(url);
return await response.json();
}
// ...
}
Changing Dependencies without Changing Interface
Extending Functionality
// Http.js
import Cache from './Cache.js' // some random cache implementation of your choice
export default class Http{
async get(url){
const cached = Cache.get(url)
if(cached) return cached
const response = await fetch(url);
return await response.json();
}
// ...
}
Best practices
Detailed prop definitions
// This is only OK when prototyping
props: ['status']
Bad
props: {
status: {
type: String,
required: true,
validator: function (value) { /* ... */ }
}
}
Best
Vue.component('my-component', {
props: {
propA: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
}
}
})
Custom prop validators
Simple expressions in templates
{{
fullName.split(' ').map(function (word) {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}}
Bad
<!-- In a template -->
{{ normalizedFullName }}
// The complex expression has been moved to a computed property
computed: {
normalizedFullName: function () {
return this.fullName.split(' ').map(function (word) {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}
}
Best
Vue.component('my-functional-component', {
functional: true,
// Props are optional
props: {
// ...
},
// To compensate for the lack of an instance,
// we are now provided a 2nd context argument.
render: function (createElement, context) {
return createElement('p', 'Functional component')
}
})
<template functional>
<p>{{ props.someProp }}</p>
</template>
<script>
export default {
props: {
someProp: String
}
}
</script>
Functional components
const EmptyList = { /* ... */ }
const TableList = { /* ... */ }
const OrderedList = { /* ... */ }
const UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
render: function (createElement, context) {
function appropriateListComponent () {
const items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
}
})
Example
Dima Vishnevetsky
@dimshik100
www.dimshik.com