Skip to content

How to Choose Between Different Code Reusable Pattern in Vue.js?

June 28, 2021Théo Dollé10 min read

a jigsaw piece is placed in a puzzle

As developers at Theodo, we are always looking for efficient ways to reuse our code without repeating ourselves (DRY principle). In the context of an e-commerce project using the Vue Storefront framework and Vue 2, we had the opportunity to study the best ways to reuse functional behaviors in Vue.

We found a lot of solutions in the Vue documentation such as Mixins, composition API and scoped slots. I hope this article will help you choose yours according to your projects.

Here are the key questions I will answer thereafter:

  • What is the best method to share logic among code in Vue 2?
  • What are the advantages and drawbacks of using the newly developed composition API, scoped slots, or Mixins to reuse some logic?
  • When should you use one over the other?

TL;DR:

  • Using Mixins is one of the ways to manage shareable logic in Vue. But they have many drawbacks and should be avoided in both Vue 2 and Vue 3.
  • Scoped slots are a great alternative to Mixins for Vue 2 and they still have their place in Vue 3.
  • The Vue 3 composition API is a powerful tool to create readable component shareable logic. It is the option I would recommend to use in most cases but is for now limited to Vue 3.

Context

We had the responsibility to develop different authentication connections for a web platform (Google & Facebook OAuth in addition to a basic email/password authentication).

In short, we needed to create three buttons that trigger three different actions. Our buttons looked very similar so we wanted to avoid duplicating the UI code. We thus created a single LoginButton.vue taking 2 props: label and icon. The button triggers an event when clicked (event named click).

// components/LoginButton.vue

<template>
  <button @click="$emit('click')">
    <img v-if="icon" :src="icon.src" alt=""/>
    <span>{{ label }}</span>
  </button>
</template>

<script>
export default {
  props: {
    icon: {
      type: Object,
    },
    label: {
      type: String,
      required: true
    }
  }
}
</script>

Our objective was to:

  • build the three authentication processes using this generic login button.
  • organize our three different login logics in a way that would allow them to be used anywhere in the app without repeating ourselves.
  • keep the code as readable as possible

Before even starting, we wondered if creating basic js functions for each connection methods in a separate JS file would be enough. The issue is that the implementation of an oauth connection requires access to the component lifecycle hooks (the initialization of the oauth client has to be triggered in the mounted hook). This access to the component options thus became a new requirement in our quest for the best solution.

First lead: Define the different connection methods within a wrapping component

This was our first idea. We created a login page component (LoginPage.vue) which uses our login button and defines three methods within it: googleLogin, facebookLogin, and emailLogin. These methods are called when a button triggers its click event.

<!-- LoginPage.vue -->

<template>
    <div class="login-page">
        <button-login label="Login with Google" :icon="googleIcon" @click="loginWithGoogle"/>
        <button-login label="Login with Facebook" :icon="facebookIcon" @click="loginWithFacebook"/>
        <button-login label="Login with email" :icon="emailIcon" @click="loginWithEmail"/>
    </div>
</template>

<script>
import ButtonLogin from './components/ButtonLogin.vue'

export default {
  name: 'LoginPage',
  components: {
    ButtonLogin
  },
  data () {
    return {
      googleIcon: {src: 'path/to/google-icon.png'},
      facebookIcon: {src: 'path/to/facebook-icon.png'},
      emailIcon: {src: 'path/to/email-icon.png'},
    }
  },
  methods: {
    loginWithGoogle () { /* google login logic */ },
    loginWithFacebook () { /* facebook login logic */ },
    loginWithEmail () { /* facebook login logic */ }
  }
}
</script>

This solution had nevertheless many drawbacks as our three login logics are contained in one single component. Consequently:

  • the LoginPage component is big and unclear
  • each connection methods is not reusable in other parts of the platform

By looking at the vue docs, we heard about Mixins. At first sight, we believed it was a great solution for the reusability of our login logics.

Second lead: Define each login logic in a different Mixin

A Mixin is an object containing component options (data, computed, methods...) that can be "merged" with a Vue component. During this process, the Mixin options are mixed with the component's options.

How does a Mixin look?

In our case, here was the GoogleLogin Mixin skeleton:

// mixins/GoogleLogin.js

export const GoogleLogin = {
  name: 'GoogleLogin',
  mounted () {
    /* init gapi client */
  },
  methods: {
    loginWithGoogle () {
      /* logic to open google popup, retrieve the authorization code and send it to backend */
    },
    onFailure () {
      /* logic when the sign in method fails */
    },
    onSuccess () {
      /* Logic sending the authorization code to my backend */
    }
  }
}

and our LoginPage:

// LoginPage.vue

<template>
  <div class="login-page">
    <button-login label="Login with Google" :icon="googleIcon" @click="loginWithGoogle"/>
    <button-login label="Login with Facebook" :icon="facebookIcon" @click="loginWithFacebook"/>
    <button-login label="Login with email" :icon="emailIcon" @click="loginWithEmail"/>
  </div>
</template>

<script>
import ButtonLogin from './components/ButtonLogin.vue'
import { GoogleLogin } from './mixins/GoogleLogin.ts'
import { FacebookLogin } from './mixins/FacebookLogin.ts'
import { EmailLogin } from './mixins/EmailLogin.ts'
export default {
  name: 'App',
  components: {
    ButtonLogin
  },
  mixins: [GoogleLogin, FacebookLogin, EmailLogin],
  data () {
    return {
      googleIcon: { src: 'path/to/google-icon.png', alt: 'google icon' },
      facebookIcon: { src: 'path/to/facebook-icon.png', alt: 'facebook icon' },
      emailIcon: { src: 'path/to/email-icon.png', alt: 'email icon' },
    }
  },
}
</script>

Mixins looked like an efficient way to encapsulate and share the different login logics through our application. Nevertheless, after a few tests, we experienced some strange behaviors while using Mixins:

  • implicit dependencies: the Mixin's methods and data are not defined within the LoginPage component. As such it is confusing to use "hypothetical" methods or data (and to not know where each method or data is defined).
  • naming collision: when merging the Mixins inside the page component, some of our methods were not called. They were overridden by methods with the same name but defined in another Mixin. As an example, if the FacebookLogin and GoogleLogin Mixins have an onFailure method, only the one defined in FacebookLogin will be called. Indeed, the FacebookLogin Mixin is imported after the GoogleLogin Mixin. In addition, if an onFailure method is also defined in the PageLogin component, then calling this.onFailure will trigger the onFailure method defined inside PageLogin. Consequently, none of GoogleLogin and FacebookLogin onFailure methods will be called (and this will happen without any warning or errors in the console).

We added to the list of our criteria the prevention of implicit dependencies and naming collision.

Knowing that, we were determined not to use Mixins. We decided to find a cleaner way to meet our objectives. It was at that moment we heard about the new composition API of Vue 3.

Third lead : Use the Vue 3 composition API

On September 18th, 2020, Vue 3 was officially released with a new great feature for reusability: the composition API. We won't get into the details of how the composition API works, but in a few words, this API is inspired by React Hooks*. It enables encapsulating component logic into small fragments of code reusable in different components.

* It has nevertheless a major difference: composition API hooks are called once and uses Vue's reactivity system while react hooks can be triggered multiple times during rendering

As an example, we could write this code fragment for google authentication logic:

// src/composables/useGoogleAuthentication.js

import { onMounted } from 'vue'

export default function useGoogleAuthentication() {
  const initGapiClient = async () => {
    /* initialization of google api client */
  }
  const loginWithGoogle = async () => {
    /* logic to open google popup, retrieve the authorization code and send it to backend */
  }

  onMounted(initGapiClient)

  return {
    loginWithGoogle
  }
}

Then, to retrieve the pieces of information required on our login page, the useGoogleAuthentication options can be merged into our page options (notably data, methods, and computed data but also lifecycle hooks). This merge is managed with the setup LoginPage component option.

// LoginPage.vue
<template>
  <div class="login-page">
    <button-login label="Login with Google" :icon="googleIcon" @click="loginWithGoogle"/>
  </div>
</template>

<script>
import ButtonLogin from './components/ButtonLogin.vue'
import useGoogleAuthentication from './composables/useGoogleAuthentication'


export default {
  name: 'App',
  components: {
    ButtonLogin
  },
  data () {
    return {
      googleIcon: { src: 'path/to/google-icon.png', alt: 'google icon' },
    }
  },
  setup() {
    const {loginWithGoogle} = useGoogleAuthentication();
    return {loginWithGoogle};
  },
}
</script>

* In order to prevent the loginWithGoogle method from triggering twice you will need to remove the custom click event in LoginButton.vue (@click="$emit('click') is no more necessary in Vue 3, the root element of a component will inherit the attribute)

As you can see, this solution prevents any implicit dependencies or naming collisions as you now need to clearly import the reusable part of your code. Reusing the authentication logic in different parts of the application is straight-forward.

This solution seems to be fitting our needs, right?

Unfortunately, we were working with a framework that was not yet supporting Vue 3. We had to find a workaround.

Good to know: As planned in the vue 3 roadmap, and especially because the support for IE may be stopped for Vue 3, some backport compatible features will be added to Vue 2.7. Among which the Composition API (the composition api plugin will be merged to Vue 2 Core). This change may enable Vue 2 users to benefit from the composition API features.

Solution: Use scoped slots

Looking a bit deeper in the Vue 2 documentation, we found the v-slot attribute (formerly named slot-scope). This attribute allows us to retrieve data or methods from a child component and pass it to the children of the component. Thus, for each connection method, we created a component including the connection generic logic and making the login method available to its children.

Here is an example with the GoogleAuth component. It encapsulates the google login logic and passes on loginWithGoogle method to its children.

// components/GoogleAuth.vue

<template>
  <div>
    <slot :loginWithGoogle="loginWithGoogle" />
  </div>
</template>

<script>
export default {
  data () {
    return {
      auth_code: ''
    }
  },
  mounted () {
    /* init gapi client */
  },
  methods: {
    loginWithGoogle () {
      /* logic to open google popup, retrieve the authorization code and send it to backend */
    },
    onFailure () {
      this.$emit('auth_error', { error: 'google error' });
    },
    onSuccess () {
      this.$emit('auth_success');
    },
  }
}
</script>

Then, the component GoogleAuth will wrap a button login and pass the login method to it. (Two other similar wrapping components for Facebook or email have to be created)

// LoginPage.vue

<template>
  <div class="login-page">
    <google-auth
      @auth_error="() => {/* manage authentication errors */}"
      @auth_success="() => {/* manage authentication success (e.g., navigate to another page) */}">
      <template v-slot="{loginWithGoogle}">
        <button-login label="Login with Google" :icon="googleIcon" @click="loginWithGoogle"/>
      </template>
    </google-auth>
  </div>
</template>

<script>
import ButtonLogin from './components/ButtonLogin.vue'
import GoogleAuth from './components/GoogleAuth.vue'
export default {
  name: 'App',
  components: {
    ButtonLogin,
    GoogleAuth
  },
  data () {
    return {
      googleIcon: { src: 'path/to/google-icon.png', alt: 'google icon' },
    }
  },
}
</script>

This solution answered our needs as it:

  • encapsulates the login logics in reusable components
  • splits the responsibility of the UX component (button) and the functional behavior (login logic)
  • doesn't create any uncontrolled side effects (each login method is scoped at the level of the button and not the page)

Conclusion

All the examples presented above allow us to compare different options to write and share component logic in Vue JS. In your project, you may choose a different option depending on the following criteria:

Criteria JS Functions Mixins Scoped slots Composition API
Encapsulate and reuse logic
Access to component options such as lifecycle hooks
No implicit or magic dependencies
No data collision
Readibility ✅ small component
❌ big component
Where can shared logic be used ? template &
component
template &
component
template template &
component
Available in Vue 2
Available in Vue 3

In a nutshell, for medium or big projects in Vue3, I would highly recommend using the composition API as it enables to write readable and reusable component logic anywhere in the code (including outside of components). For Vue 2, use scoped slots (for now) as it is a pretty straight-forward way to share component logic in Vue 2 without the drawbacks of mixins. It is even the best option for code reuse when dealing with a reusable component that provides a customizable UI in both Vue 2 and Vue 3.

Théo Dollé

Théo Dollé

Theodo - Developer