DEV Community

loading...
Cover image for Vue.js - Cleaning up components

Vue.js - Cleaning up components

michi profile image Michael Z Originally published at michaelzanggl.com Updated on ・5 min read

Originally posted at michaelzanggl.com. Subscribe to my newsletter to never miss out on new content.

If you write a semi-big vue application you might see familiar patterns popping up repeatedly. Let's take a look at some of these and how we can drastically improve our Vue components.

This is the component we will refactor. Its purpose is to fetch a list of threads. It also handles cases when the thread list is empty, when the component is currently fetching the resource or when there was an error fetching the resource. This currently results in over 50 lines of code.

<template>
<div v-if="error">
  Whoops! Something happened
</div>
<div v-else-if="isPending">
  <LoadingSpinner />
</div>
<div v-else-if="isEmpty" class="empty-results">
  There are no threads!
</div>
<div v-else>
  <ThreadList :threads="threads" />
</div>
</template>
<script>
import LoadingSpinner from '../layouts/LoadingSpinner'
import ThreadList from './ThreadList'

export default {
  components: { LoadingSpinner, ThreadList },
  data() {
    return {
        threads: [],
        error: null,
        isPending: true,
    }
  },
  computed: {
    isEmpty() {
      return !this.isPending && this.threads.length < 1
    }
  },
  async created() {
    try {
      this.threads = await fetch('/api/threads').then(res => res.json())
    } catch (error) {
      this.error = error
    }

    this.isPending = false
  }
}
</script>
<style scoped>
.empty-results {
  margin: 1rem;
  font-size: .875rem;
  text-align: center;
}

@media (min-width: 1024px) {
  margin: .875rem;
}
</style>
Enter fullscreen mode Exit fullscreen mode

There are a ton of improvements we can do without reaching for a state management library like vuex, so let's check them out one by one.

Note that none of these improvements are strictly necessary, but keep the ones you like in your head for the time you do feel like writing components becomes cumbersome.


1. Global components

If you have some general component that you need on a lot of pages it can make sense to register it as a global component. This is exactly the case with our LoadingSpinner.

To register it globally, head over to the file where you instantiate vue, you know, where you also register any modules using Vue.use.

Here, we can now import the loading spinner and register it globally.

import LoadingSpinner from './layouts/LoadingSpinner'

Vue.component('LoadingSpinner', LoadingSpinner)

// ...
// new Vue()
Enter fullscreen mode Exit fullscreen mode

And that's it! Now you can remove the import and component registration from our component, leaving us with:

// ...

<script>
import ThreadList from './ThreadList'

export default {
  components: { ThreadList },
  // ...
Enter fullscreen mode Exit fullscreen mode

2. Error Boundary

Catching errors in every component can become quite cumbersome. Luckily there is a solution for that.

Let's create a new component called ErrorBoundary.vue.

<template>
<div v-if="!!error">
    Whoops! {{ error }}
</div>
<div v-else>
    <slot></slot>
</div>

</template>
<script>
export default {
    data: () => ({
        error: null,
    }),

    errorCaptured (error, vm, info) {
        this.error = error
    },
}
</script>
Enter fullscreen mode Exit fullscreen mode

This is an ErrorBoundary component. We wrap it around components and it will catch errors that were emitted from inside those components and then render the error message instead. (If you use vue-router, wrap it around the router-view, or even higher)

For example:

<template>
<v-app>
  <ErrorBoundary>
    <v-content>
      <v-container fluid>
        <router-view :key="$route.fullPath"></router-view>
      </v-container>
    </v-content>
  </ErrorBoundary>
</v-app>
</template>

<script>
import ErrorBoundary from './layout/ErrorBoundary'

export default {
  components: {
    ErrorBoundary,
  }
}
Enter fullscreen mode Exit fullscreen mode

Nice! Back in our component we can now get rid of the error property and the if condition in the template:

<div v-if="error">
  Whoops! Something happened
</div>
Enter fullscreen mode Exit fullscreen mode

And our created lifecycle method no longer requires the try-catch:

async created() {
    this.threads = await fetch('/api/threads').then(res => res.json())
    this.isPending = false
  }
Enter fullscreen mode Exit fullscreen mode

3. Utility first CSS

Vue's scoped CSS is truly an amazing feature. But let's see if we can get this even simpler. If you followed some of my previous blog posts you will know I am a big fan of utility first CSS. Let's use tailwind CSS here as an example, but you could potentially also create your own global utility classes to kick things off.

After installing tailwindCSS we can remove all of this

<style scoped>
.empty-results {
  margin: 1rem;
  font-size: .875rem;
  text-align: center;
}

@media (min-width: 1024px) {
  margin: .875rem;
}
</style>
Enter fullscreen mode Exit fullscreen mode

And in our template, the following:

<div v-else-if="isEmpty" class="empty-results">
  There are no threads!
</div>
Enter fullscreen mode Exit fullscreen mode

now becomes:

<div v-else-if="isEmpty" class="m-4 lg:m-3 text-sm text-center">
  There are no threads!
</div>
Enter fullscreen mode Exit fullscreen mode

If you find yourself repeating these classes, put the div in a dumb component!
If, on the other hand you find this to be an absolutely horrible way to do CSS, please check out my blog post explaining this approach.

4. promistate

There is still a lot of code that needs to be repeated across similar components, especially this part here:

<script>
export default {
  data() {
    return {
        threads: [],
        isPending: true,
    }
  },
  computed: {
    isEmpty() {
      return !this.isPending && this.threads.length < 1
    }
  }
  // ...
}
</script>
Enter fullscreen mode Exit fullscreen mode

For this, I have written my own little library called promistate to simplify "promised" state like this.

Using promistate the script now becomes:

<script>
import ThreadList from './ThreadList'
import promistate from 'promistate'

export default {
  components: { ThreadList },
  data() {
    const threadsPromise = promistate(() => fetch('/api/threads').then(res => res.json()), { catchErrors: false }) // no fetch fired yet

    return { threadsPromise }
  },
  async created() {
    await this.threadsPromise.load() // callback gets fired and saved inside this object
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

and the template becomes:

<template>
<div v-if="threadsPromise.isPending">
  <LoadingSpinner v-if="threadsPromise.isPending" />
</div>
<div v-else-if="threadsPromise.isEmpty" class="m-4 lg:m-3 text-sm text-center">
  There are no threads!
</div>
<div v-else>
  <ThreadList :threads="threadsPromise.value" />
</div>
</template>
Enter fullscreen mode Exit fullscreen mode

You can check the promistate documentation for how it works, but basically we simply store the callback you pass in inside data and when you trigger the callback using the load method it sets values like isPending, isEmpty etc.
We also pass the option catchErrors: false so the error keeps bubbling up to our ErrorBoundary. You can now decide for yourself if you still need that ErrorBoundary though.

You can even go a step further and create a component that accepts a promise to automatically handle the pending, empty and error states.

5. Remove useless divs

Let's take a look at our template once more. There are quite a few divs inside that we don't actually need. Removing those results in simply

<LoadingSpinner v-if="threadsPromise.isPending" />
<div v-else-if="threadsPromise.isEmpty" class="m-4 lg:m-3 text-sm text-center">
  There are no threads!
</div>
<ThreadList v-else :threads="threadsPromise.value" />
</template>
Enter fullscreen mode Exit fullscreen mode

Alright! Down to 23 lines.

6. Give your code some room to breathe

So far we focused a lot on reducing the LOC (lines of code) in our vue component. But focusing on this one criteria alone could get our code into an equally bad shape as we had before...

I love it when Steve Schoger talks about design, he always says to give your elements more room to breathe. The same can also apply to code!

In fact, I think our component can greatly benefit from adding some space.

Turning

<template>
<LoadingSpinner v-if="threadsPromise.isPending" />
<div v-else-if="threadsPromise.isEmpty" class="m-4 lg:m-3 text-sm text-center">
  There are no threads!
</div>
<ThreadList v-else :threads="threadsPromise.value" />
</template>
<script>
import ThreadList from './ThreadList'
import promistate from 'promistate'

export default {
  components: { ThreadList },
  data() {
    const threadsPromise = promistate(() => fetch('/api/threads').then(res => res.json()), { catchErrors: false })

    return { threadsPromise }
  },
  async created() {
    await this.threadsPromise.load()
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

into

<template>
<LoadingSpinner v-if="threadsPromise.isPending" />

<div v-else-if="threadsPromise.isEmpty" class="m-4 lg:m-3 text-sm text-center">
  There are no threads!
</div>

<ThreadList v-else :threads="threadsPromise.value" />
</template>

<script>
import ThreadList from './ThreadList'
import promistate from 'promistate'

export default {
  components: { ThreadList },

  data() {
    const threadsPromise = promistate(() => fetch('/api/threads').then(res => res.json()), { catchErrors: false })
    return { threadsPromise }
  },

  async created() {
    await this.threadsPromise.load()
  }
}
</script>
Enter fullscreen mode Exit fullscreen mode

At least for me, this reads a lot easier.


And there you have it, 6 ways to clean up your Vue components. Let's see how the composition API in Vue 3 will change things again!

Discussion (4)

pic
Editor guide
Collapse
drewtownchi profile image
Drew Town

Just curious why you decided to create promistate? There are a few other I have seen that do similar things (such as vue-promised) and I was curious as to what you would consider the main differences to be?

Error boundary is a really good tip and one that I personally way under-utilize. I'm definitely going to try and incorporate that into my projets more.

Collapse
michi profile image
Michael Z Author

It's a more low level approach that allows devs to implement their own version of vue-promised (if they want to), also works nicely with things like form submits where you need similar properties.

Collapse
richardeschloss profile image
Richard Schloss

Also, it seems like promisestate is framework agnostic and front-end/back-end agnostic, right? So you can re-use it where-ever you want.

Thread Thread
michi profile image
Michael Z Author

Yup, that's true as well!