Upcoming and OnDemand Webinars View full list

Designing Effective Vue Components

Dane Gilbert

While developing a Vue application it is common to create many different components to provide reusable logic and structure. While there are various design methodologies to accomplish this, let’s look at some Vue specific tools that can make our components more effective.

  1. Props as an Interface
  2. Slots for Content and Layout
  3. Expose Data With Custom Events
  4. Functional Fun

Note: If you have never worked with Vue components before or if it has been a while since working with them, I would recommend a quick overview of the docs for a refresher.

Props as an Interface

Beyond being the usual approach to pass data to components, props can also provide a way to define a contract of what input is acceptable or needed for the component to work. Let’s say we want to use a component and find out that it accepts a field called users.

{
  props: {
      users: Array
  }
}

All we know initially is that an Array is expected, but what data does it contain? Can the component work without this information? Unless we have more context, it is likely we will have to look at the component implementation to gather more information. This is a sign that our component interface could be improved on, so let’s try adding some more details.

{
  props: {
      users: {
        type: Array, // In contrast to the previous example, this now needs to be in its own field
        required: true,
        validator: (userList) => {
          for (const user of userList) {
            if (!user.hasOwnProperty("id") || user.name == "") {
              return false;
            }
          }
          return true;
        }
      }
    }
}

We now can see that the component requires the users field and also defines a validator function. The validator is a predicate function that takes in the given value for the prop and returns whether or not it is acceptable. When working on a development build, any data not provided to a required field or validators that return false will log errors to the console. Note that required defaults to false if not specified, but if a prop isn’t required, consider adding a default to provide a sensible initial value.

Slots for Content and Layout

Slots provide a great interface for inserting different content into a component, but it can be challenging to determine how to use them most effectively. One application could be to create components that simply format content in a specific way.

For example, we could create a component that organizes its children into two even-width columns:

<template>
  <div class="columns">
    <div class="left">
      <slot name="left"></slot>
    </div>
    <div class="right">
      <slot name="right"></slot>
    </div>
  </div>
</template>

<style lang="css" scoped>
.columns {
  display: flex;
}
.left {
  flex-basis: 0;
  flex-grow: 1;
}
.right {
  flex-basis: 0;
  flex-grow: 1;
}
</style>

Then all we need to do is wrap our content with this component and place content in each slot. This can be an effective tool for simplifying complex layouts and workflows, but keep in mind that we are constrained by the layout of the wrapper component. This usually is an acceptable constraint, but be conscious of refactoring styles in a frequently used component.

Expose Data With Custom Events

Where props provide data down to child components, events allow pushing data upwards to an immediate parent through several approaches:

  • Listen to the native event of the component’s root element using the .native modifier
  • Have the component use the $listeners object to hook up provided listeners to the correct element(s)
  • Use $emit to dispatch a custom event

The first option allows a user to listen to events from the component’s root element, but this is subject to breaking without warning if the root element changes. The second option lets the component decide how the handlers should be applied which solves the coupling issue faced by .native, but this probably isn’t ideal for a complex component with lots of interactive fields. Custom events provide the flexibility of choosing what data to expose in the format that we want.

We dispatch our own custom events using $emit(eventName, [...args]) within a component.

// Note to always use kebab-case for the event name as to avoid issues with casing
this.$emit("user-change", { id: 1, name: "Vuelysses" });

Then we can listen for the event directly on the component, just like a regular DOM element.

<OurCustomComponent @user-change="userChangeHandler" />

This is great because it allows the component to dictate what information it wants to pass up without the user having to worry about internal structure and events. Just keep in mind one subtle difference between a custom event and an event dispatched from a DOM element is that data from a DOM event will conform to the Event interface, while a custom event’s data will be whatever is provided to the $emit call. Really this just means if you end up changing from a DOM event to a custom event make sure your consumers can handle the change in format.

Functional Fun

When talking about functional programming there is often discussion about avoiding side effects and solving common problems with higher-order functions. So what does it mean for a Vue component to be functional? Components in Vue are functional when they don’t manage a data instance and don’t use the this keyword, as they aren’t instantiated. When these requirements are met, you can add the functional property to your component, which will let Vue know it doesn’t need to instantiate your component, making it cheaper to render.

Functional components are often used as wrappers for certain functionality. As a simple example, say we want a component that shows a loading icon while it’s in a loading state and then displays its children when loading is false.

<script>
import LoadingGif from "./LoadingGif.vue"; // Some external component that displays the actual loading gif
export default {
  props: {
    loading: {
      type: Boolean,
      required: true
    }
  },
  functional: true,
  render(createElement, context) {
    if (context.props.loading) {
      return createElement(LoadingGif);
    }
    return context.children;
  }
};
</script>

Then use it on a component that needs a little time to load

<template>
  <div>
    <LoadingWrapper :loading="loading">
      <p>Loaded!</p>
    </LoadingWrapper>
  </div>
</template>

<script>
// Our functional component
import LoadingWrapper from "./LoadingWrapper";
export default {
  components: {
    LoadingWrapper
  },
  data() {
    return {
      loading: true
    };
  },
  mounted() {
    setTimeout(() => (this.loading = false), 2000);
  }
};
</script>

Pretty straightforward, but that’s what makes it easy to use and possibly extend further!

Conclusion

Hopefully this gave a little more insight into some of Vue’s capabilities and what tools are available for making effective components. With future releases of Vue we will hopefully see even more growth and ways to improve on what we have now.

Thanks for reading!

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project