Next-Level Vue 2

Lessons

1. Next-Level Vue: Orientation

3:20

2. Progress Bar: Axios Interceptors

8:30

3. In-Component Route Guards

7:48

4. Global and Per-Route Guards

10:13

5. Completing our Progress Bar

11:08

6. Error Handling

10:04

7. Reusable Form Components: BaseInput

8:06

8. Reusable Form Components: BaseSelect

6:13

9. Reusable Form Components: BaseButton

5:58

10. Form Validation with Vuelidate

10:02

11. Form Validation with Vuelidate Pt. 2

12:53

12. Mixins

7:59

13. Filters

7:31

Mixins

It’s common when developing an application full of components to reach a point where you have two components that contain very similar functionality. The question then is: do we fuse those two components into one? Or do we keep them separate since there’s enough different about each of them that combining them would cause more problems than it would solve?

Fortunately, Vue gives us a third option: Mixins. Mixins are a useful way to encapsulate a small piece of functionality that can be reused across multiple components. In this tutorial, we’ll be learning how to create and efficiently make use of this handy tool.


A Simple Example

To start getting familiar with Mixins, let’s look at a simple example.

📃 exampleMixin.js

    export const exampleMixin = {
      created() {
        console.log('Hello from the mixin!')
      }
    }

As you can see, we’ve created a new .js file that exports our exampleMixin, which just logs a message to the console when the component is created.

Now we can import this mixin into an example component and list it on that component’s mixins option in order to extend that component’s functionality.

📃Example.vue

    <script>
    import { exampleMixin } from '../mixins/exampleMixin.js'
      export default {
        mixins: [exampleMixin]
      }
    </script>

Now, when our Example component is created, we should see the console.log from our exampleMixin appear.

Great. So far so good.


Order of Operations

The way that mixins are handled, the mixin code is run prior to the component code. To see this in action, let’s look at how this code will run:

If we have this same mixin:

📃 exampleMixin.js

    export const exampleMixin = {
      created() {
        console.log('Hello from the mixin!')
      }
    }

And this component:

📃Example.vue

    <script>
    import { exampleMixin } from '../mixins/exampleMixin.js'
    export default {
      mixins: [exampleMixin],
      created() {
        console.log('Hey from the component.')
      }
    }
    </script>

Now our component itself is also supposed to be logging a message to the console when it is created. So what do you think we’ll see in the console? In what order do you expect to see the logs?

If we check the console, we see:

Indeed, our mixin code was run just before our component’s code, as we can see by the order of our console logs. Under the hood, both of our created hook functions were merged into an array, with the mixin’s hook being the first element in that array and our component’s hook being the second. Then they were run in that order, hence the order in which we see the console logs.


Mixing in More

Now what if needed to mix-in more than just a simple lifecycle hook? We aren’t limited to just hooks, we can mix-in any component option, such as methods, data, props, etc.

For example, we might have a mixin that looks like this:

exampleMixin.js

    export const exampleMixin = {
      created() {
        this.logMessage()
      },
      data() {
        return {
          message: 'I am such a nice mixin.'
        }
      },
      methods: {
        logMessage() {
          console.log(this.message)
        }
      }
    }

Now if we imported this into a component, it would run the logMessage method when it was created, printing the message data to the console. However, that is assuming none of what is on this mixin conflicts with what is on our component. Fortunately, the way Vue handles mixins helps us automatically avoid conflicts when they arise. Let’s look at how that works.


Mixing in Data

In cases where we want to mix-in data, data objects undergo a recursive merge. If there are conflicts, the component’s data takes priority. Let’s look at an example.

If we had this mixin:

📃exampleMixin.js

    export const exampleMixin = {
      data() { return { message: 'I am secondary.' } }
    }

And this component:

📃Example.vue

    <script>
    import { exampleMixin } from '../mixins/exampleMixin.js'
    export default {
      mixins: [exampleMixin],
      data() {
        return {
          message: 'I take priority.'
        }
      },
      created() {
        console.log(this.message)
      }
    }
    </script>

Which message do you think will show up in the console? Looking in the console, we’ll see that the component’s data won out.

Vue recognized there was a conflict between the mixin’s message data and the component’s message data, so it chose to use the component’s own code. Again, the component’s code will always take priority in conflicts like this.


Merging Object Options

Now that we understand how hooks and data are merged, what about options that expect an object such as our methods or props option? As we might expect by now, the component’s options will take priority when there are conflicting keys in those merged objects.

For example, if we had this mixin, which has a hello method:

📃exampleMixin.js

    export const exampleMixin = {
      methods: {
        hello() {
          console.log('Hello from the mixin!')
        }
      }
    }

And this component, which has its own hello method:

📃Example.vue

    <script>
    import { exampleMixin } from '../mixins/exampleMixin.js'
    export default {
      mixins: [exampleMixin],
      methods: {
        hello() {
          console.log('Hello from the component.')
        }
      }
    }
    </script>

What will we see in the console?

That’s right - we’d see the output of our component’s hello method since that took priority over the conflicting hello method from our mixin.


Global Mixins

It’s also worth noting that we are able to create global mixins, which would be applied to each and every component within our application.

In a Vue CLI-generated project, we’d create a global mixin in our main.js file, just above the main Vue instance.

📃main.js

    Vue.mixin({
      mounted() {
        console.log('I am mixed into every component.')
      }
    })
    
    new Vue({
      ...
    })

However, the use cases for a global mixin are rare and it is highly recommended that you use global mixins with extreme caution or simply avoid using them at all since they will be affecting literally every component at every level of your app.



A Next-Level Mixin

Now that we understand the basics of how mixins work to extend the functionality of our components, let’s look at an example that’s more… Next-Level.

Specifically, if we look at how we’ve written our BaseSelect and BaseInput components, we’ll notice that we’re repeating some code within each of them.

📃BaseSelect.vue

    <template>
      <div>
        <label v-if="label">{{ label }}</label>
        <select :value="value" @change="updateValue" v-bind="$attrs" v-on="$listeners">
          <option
            v-for="option in options"
            :value="option"
            :key="option.id"
            :selected="option === value"
          >{{ option }}</option>
        </select>
      </div>
    </template>
    <script>
    export default {
      inheritAttrs: false, 
      props: {
        options: {
          type: Array,
          required: true
        },
        value: [String, Number],
        label: String
      },
      methods: {
        updateValue(event) {
          this.$emit('input', event.target.value)
        }
      }
    }
    </script>

📃BaseInput.vue

    <template>
      <div>
        <label v-if="label">{{ label }}</label>
        <input :value="value" @input="updateValue" v-bind="$attrs">
      </div>
    </template>
    <script>
    export default {
      inheritAttrs: false,
      props: {
        label: {
          type: String,
          default: ''
        },
        value: [String, Number]
      },
      methods: {
        updateValue(event) {
          this.$emit('input', event.target.value)
        }
      }
    }
    </script>

Both components contain inheritAttrs: false, along with a label and value prop and an updateValue method. We can encapsulate this repetitive code into its own mixin that both form components can then use.


Creating our Mixin

Before we create our mixin, we’ll add a mixins folder within our src folder to keep things organized. Inside of our mixins folder, we’ll create a .js file for our new mixin.

📃src/mixins/formFieldMixin.js

    export const formFieldMixin = {
      inheritAttrs: false,
      props: {
        label: {
          type: String,
          default: ''
        },
         value: [String, Number]
      },
      methods: {
        updateValue(event) {
          this.$emit('input', event.target.value)
        }
      }
    }

As you can see, we’ve added all of the code that was repeated between the two components.


Using the Mixin

Since we have now encapsulated that repeated code within a new formFieldMixin, we can import it into our BaseInput and BaseSelect components, and delete the code that the mixin replaces.

📃src/components/BaseInput.vue

    <template>
      <div>
        <label v-if="label">{{ label }}</label>
        <input :value="value" @input="updateValue" v-bind="$attrs" v-on="listeners">
      </div>
    </template>
    <script>
    import { formFieldMixin } from '../mixins/formFieldMixin' // import mixin
    export default {
      mixins: [formFieldMixin], // register mixin
      props: {
        value: [String, Number]
      },
      computed: {
        listeners() {
          return {
            ...this.$listeners,
            input: this.updateValue
          }
        }
      }
    }
    </script>

📃src/components/BaseSelect.vue

    <template>
      <div>
        <label v-if="label">{{ label }}</label>
        <select :value="value" @change="updateValue" v-bind="$attrs" v-on="$listeners">
          <option
            v-for="option in options"
            :value="option"
            :key="option.id"
            :selected="option === value"
          >{{ option }}</option>
        </select>
      </div>
    </template>
    <script>
    import { formFieldMixin } from '../mixins/formFieldMixin' // import mixin
    export default {
      mixins: [formFieldMixin], // register mixin
      props: {
        options: {
          type: Array,
          required: true
        },
        value: [String, Number]
      }
    }
    </script>

Great. Now these form components appear even leaner and both rely on the same shared functionality of our formFieldMixin, making our code more DRY.


Let’s Revue

Mixins are a handy way to reuse code across our components to share behavior, making our codebase less repetitive and more modular. We looked at the order of operations of mixins, and we learned how mixin code is run prior to the component’s own code and how that helps automatically resolve conflicts that may emerge with things such as duplicated method names. We then saw how easy it can be to encapsulate code that is repeated within two places into a single mixin instead.


What’s next?

In the next lesson, we’ll take a look at ways to effectively filter within our components.