Get the most out of Vue

Successfully building ๐Ÿ— a large Vue.js 3 App

Dima Vishnevetsky

About me

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

Official Libraries and Component Libraries

pinia.vuejs.org

vitejs.dev

vuetifyjs.com

Flat
Component
Directory

  • Remove analysis paralysis when it comes to deciding how to organize components into subdirectories

  • Quickly go from spotting a component in Vue devtools to finding the file in the codebase
    (as the filename and the component name are the same)

  • Use your IDE's quick find or file jumping feature to filter files based on their most general attribute down to the more specific

  • Be able to see all your components at once in a single list

  • Remove the temptation to use short one word component names which is easier to do with nested directories
    (ie. car/List.vue, car/Feature.vue ) and violates the style guide

  • Eliminate surfing the file structure in and out of directories to find a component

  • Simplify importing components
    (will always be import SomeComponent from "@/SomeComponent")

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

Route/Page Naming Convention

Typical CRUD application has the following different pages for each resource:

  1. a list of all the resources

  2. a view of a single resource

  3. a form to create the resource

  4. 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.

Composables

  • Provide a transparent source of component data, methods, etc
    ย 
  • Eliminate naming collisions
    ย 
  • Compostables are just plain JavaScript that your IDE can interpret and autocomplete
// 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

  • Make components all props down/events upย 
    (don't have any external dependencies)
    ย 
  • Helper functions should contain reusable logic
    (can
    be tested outside of the context of a component)
    ย 
  • Helper functions should have a single responsibility
    (never allow them to create side effects)
    ย 
  • Use Vue Testing Libraryย to test components
    (It's a higher-level abstraction over (and built on top of) Vue Test Utils)

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

Thank you

Successfully building a large Vue.js 3 App ๐Ÿ—

By Dima Vishnevetsky

Successfully building a large Vue.js 3 App ๐Ÿ—

  • 532