Here are the top best practices I've developed while working on Vue projects with a large code base. These tips will help you develop more efficient code that is easier to maintain and share.

When freelancing this year, I had the opportunity to work on some large Vue applications. I am talking about projects with more than 😰 a dozen Vuex stores, a high number of components (sometimes hundreds) and many views (pages). 😄 It was actually quite a rewarding experience for me as I discovered many interesting patterns to make the code scalable. I also had to fix some bad practices that resulted in the famous spaghetti code dilemma. 🍝

Thus, today I’m sharing 10 best practices with you that I would recommend to follow if you are dealing with a large code base. 🧚🏼‍♀️

1. Use Slots to Make Your Components Easier to Understand and More Powerful

I recently wrote an article about some important things you need to know regarding slots in Vue.js. It highlights how slots can make your components more reusable and easier to maintain and why you should use them.

🧐 But what does this have to do with large Vue.js projects? A picture is usually worth a thousand words, so I will paint you a picture about the first time I deeply regretted not using them.

One day, I simply had to create a popup. Nothing really complex at first sight as it was just including a title, a description and some buttons. So what I did was to pass everything as props. I ended up with three props that you would use to customize the components and an event was emitted when people clicked on the buttons. Easy peasy! 😅

But, as the project grew over time, the team requested that we display a lot of other new things in it: form fields, different buttons depending on which page it was displayed on, cards, a footer, and the list goes on. I figured out that if I kept using props to make this component evolve, it would be ok. But god, 😩 how wrong I was! The component quickly became too complex to understand as it was including countless child components, using way too many props and emitting a large number of events. 🌋 I came to experience that terrible situation in which when you make a change somewhere and somehow it ends up breaking something else on another page. I had built a Frankenstein monster instead of a maintainable component! 🤖

However, things could have been better if I had relied on slots from the start. I ended up refactoring everything to come up with this tiny component. Easier to maintain, faster to understand and way more extendable!

<template> <div class="c-base-popup"> <div v-if="$slots.header" class="c-base-popup__header"> <slot name="header"> </div> <div v-if="$slots.subheader" class="c-base-popup__subheader"> <slot name="subheader"> </div> <div class="c-base-popup__body"> <h1>{{ title }}</h1> <p v-if="description">{{ description }}</p> </div> <div v-if="$slots.actions" class="c-base-popup__actions"> <slot name="actions"> </div> <div v-if="$slots.footer" class="c-base-popup__footer"> <slot name="footer"> </div> </div> </template> <script> export default { props: { description: { type: String, default: null }, title: { type: String, required: true } } } </script>

My point is that, from experience, projects built by developers who know when to use slots does make a big difference on its future maintainability. Way fewer events are being emitted, the code is easier to understand, and it offers way more flexibility as you can display whatever components you wish inside.

⚠️ As a rule of thumb, keep in mind that when you end up duplicating your child components' props inside their parent component, you should start using slots at that point.

2. Organize Your Vuex Store Properly

Usually, new Vue.js developers start to learn about Vuex because they stumbled upon on of these two issues:

Either they need to access the data of a given component from another one that’s actually too far apart in the tree structure, or

They need the data to persist after the component is destroyed.

That's when they create their first Vuex store, learn about modules and start organizing them in their application. 💡

The thing is that there is no single pattern to follow when creating modules. However, 👆🏼 I highly recommend you think about how you want to organize them. From what I've seen, most developers prefer to organize them per feature. For instance:

Auth.

Blog.

Inbox.

Settings.

😜 On my side, I find it easier to understand when they are organized according to the data models they fetch from the API. For example:

Users

Teams

Messages

Widgets

Articles

Which one you choose is up to you. The only thing to keep in mind is that a well-organized Vuex store will result in a more productive team in the long run. It will also make newcomers better predisposed to wrap their minds around your code base when they join your team.

3. Use Actions to Make API Calls and Commit the Data

Most of my API calls (if not all) are made inside my Vuex actions. You may wonder: why is that a good place to do so? 🤨

🤷🏼‍♀️ Simply because most of them fetch the data I need to commit in my store. Besides, they provide a level of encapsulation and reusability I really enjoy working with. Here are some other reasons I do so:

If I need to fetch the first page of articles in two different places (let's say the blog and the homepage), I can just call the appropriate dispatcher with the right parameters. The data will be fetched, committed and returned with no duplicated code other than the dispatcher call.

If I need to create some logic to avoid fetching this first page when it has already been fetched, I can do so in one place. In addition to decreasing the load on my server, I am also confident that it will work everywhere.

I can track most of my Mixpanel events inside these actions, making the analytics code base really easy to maintain. I do have some applications where all the Mixpanel calls are solely made in the actions. 😂 I can't tell you how much of a joy it is to work this way when I don't have to understand what is tracked from what is not and when they are being sent.

4. Simplify Your Code Base with mapState, mapGetters, mapMutations and mapActions

There usually is no need to create multiple computed properties or methods when you just need to access your state/getters or call your actions/mutations inside your components. Using mapState , mapGetters , mapMutations and mapActions can help you shorten your code and make things easier to understand by grouping what is coming from your store modules in one place.

// NPM import { mapState, mapGetters, mapActions, mapMutations } from "vuex"; export default { computed: { // Accessing root properties ...mapState("my_module", ["property"]), // Accessing getters ...mapGetters("my_module", ["property"]), // Accessing non-root properties ...mapState("my_module", { property: state => state.object.nested.property }) }, methods: { // Accessing actions ...mapActions("my_module", ["myAction"]), // Accessing mutations ...mapMutations("my_module", ["myMutation"]) } };

All the information you'll need on these handy helpers is available here in the official Vuex documentation. 🤩

5. Use API Factories

I usually like to create a this.$api helper that I can call anywhere to fetch my API endpoints. At the root of my project, I have an api folder that includes all my classes (see one of them below).

api ├── auth.js ├── notifications.js └── teams.js

Each one is grouping all the endpoints for its category. Here is how I initialize this pattern with a plugin in my Nuxt applications (it is quite a similar process in a standard Vue app).

// PROJECT: API import Auth from "@/api/auth"; import Teams from "@/api/teams"; import Notifications from "@/api/notifications"; export default (context, inject) => { if (process.client) { const token = localStorage.getItem("token"); // Set token when defined if (token) { context.$axios.setToken(token, "Bearer"); } } // Initialize API repositories const repositories = { auth: Auth(context.$axios), teams: Teams(context.$axios), notifications: Notifications(context.$axios) }; inject("api", repositories); };

export default $axios => ({ forgotPassword(email) { return $axios.$post("/auth/password/forgot", { email }); }, login(email, password) { return $axios.$post("/auth/login", { email, password }); }, logout() { return $axios.$get("/auth/logout"); }, register(payload) { return $axios.$post("/auth/register", payload); } });

Now, I can simply call them in my components or Vuex actions like this:

export default { methods: { onSubmit() { try { this.$api.auth.login(this.email, this.password); } catch (error) { console.error(error); } } } };

6. Use \$config to access your environment variables (especially useful in templates)

Your project probably have some global configuration variables defined in some files:

config ├── development.json └── production.json

I like to quickly access them through a this.$config helper, especially when I am inside a template. As always, it's quite easy to extend the Vue object:

// NPM import Vue from "vue"; // PROJECT: COMMONS import development from "@/config/development.json"; import production from "@/config/production.json"; if (process.env.NODE_ENV === "production") { Vue.prototype.$config = Object.freeze(production); } else { Vue.prototype.$config = Object.freeze(development); }

7. Follow a Single Convention to Name Your Commits

As the project grows, you will need to browse the history for your components on a regular basis. If your team does not follow the same convention to name their commits, it will make it harder to understand what each one does.

I always use and recommend the Angular commit message guidelines. I follow it in every project I work on, and in many cases other team members are quick to figure out that it's better to follow too.

Following these guidelines leads to more readable messages that make commits easier to track when looking through the project history. In a nutshell, here is how it works:

git commit -am "<type>(<scope>): <subject>" # Here are some samples git commit -am "docs(changelog): update changelog to beta.5" git commit -am "fix(release): need to depend on latest rxjs and zone.js"

Have a look at their README file to learn more about it and its conventions.

8. Always Freeze Your Package Versions When Your Project is in Production

I know... All packages should follow the semantic versioning rules. But the reality is, some of them don't. 😅

To avoid having to wake up in the middle of the night because one of your dependencies broke your entire project, locking all your package versions should make your mornings at work less stressful. 😇

What it means is simply this: avoid versions prefixed with ^ :

{ "name": "my project", "version": "1.0.0", "private": true, "dependencies": { "axios": "0.19.0", "imagemin-mozjpeg": "8.0.0", "imagemin-pngquant": "8.0.0", "imagemin-svgo": "7.0.0", "nuxt": "2.8.1", }, "devDependencies": { "autoprefixer": "9.6.1", "babel-eslint": "10.0.2", "eslint": "6.1.0", "eslint-friendly-formatter": "4.0.1", "eslint-loader": "2.2.1", "eslint-plugin-vue": "5.2.3" } }

9. Use Vue Virtual Scroller When Displaying a Large Amount of Data

When you need to display a lot of rows in a given page or when you need to loop over a large amount of data, you might have noticed that the page can quickly become quite slow to render. To fix this, you can use vue-virtual-scoller.

npm install vue-virtual-scroller

It will render only the visible items in your list and re-use components and dom elements to be as efficient and performant as possible. It really is easy to use and works like a charm! ✨

<template> <RecycleScroller class="scroller" :items="list" :item-size="32" key-field="id" v-slot="{ item }" > <div class="user"> {{ item.name }} </div> </RecycleScroller> </template>

10. Track the Size of Your Third-Party Packages

When a lot of people work in the same project, the number of installed packages can quickly become incredibly high if no one is paying attention to them. To avoid your application becoming slow (especially on slow mobile networks), I use the import cost package in Visual Studio Code. This way, I can see right from my editor how large an imported module library is, and can check out what's wrong when it's getting too large.

For instance, in a recent project, the entire lodash library was imported (which is approximately 24kB gzipped). The issue? Only the cloneDeep method was used. By identifying this issue with the import cost package, we fixed it with:

npm remove lodash npm install lodash.clonedeep

The clonedeep function could then be imported where needed:

import cloneDeep from "lodash.clonedeep";

⚠️ To optimize things even further, you can also use the Webpack Bundle Analyzer package to visualize the size of your webpack output files with an interactive zoomable treemap.

Do you have other best practices when dealing with a large Vue code base? Feel free to tell me in the comments below or to reach out to me on Twitter @RifkiNada. 🤠