Search

From React to Vue: Re-Vue-sable Components

Sam Kwak

7 min read

Jan 20, 2019

From React to Vue: Re-Vue-sable Components

From React to Vue: Re-Vue-sable Components

For many developers, the most beloved part of any modern front-end framework is the ability to create clear and modular components. React took off like a storm by celebrating its “Just JavaScript” roots, bringing with it a simple but robust API for composing components together into advanced user interfaces. Today, React is not the only option for this style of development. Whereas React delegates design patterns to the developer, Vue.js comes prepackaged with a suite of APIs for creating powerful, reusable components. By leveraging these APIs, any developer can produce a robust Vue.js application.

Vue.js 2.x has two primary approaches for introducing reusability beyond components: mixins and slots. There are a few other options with their own use cases, such as custom directives and JSX inside render functions, but we’ll focus on mixins and slots, as they’ll be applicable to a majority of use cases.

If you’d like to play with any of the examples below, check out my CodeSandbox!

If the <template> keyword is new to you, it essentially takes the place of React’s render function. Check out single file components in Vue.js for some more details.

Mixins

One of the most immediate reuse concerns with components is that different components will often require identical blocks of code. Utility and helper functions can offload a good part of that, but there comes a point where code is reliant on component lifecycle or properties, and copying code really is the most valid solution. Mixins are a programmatic way of doing just that, spreading one block of component-aware code across multiple components. Any option that a component may have, so too can a mixin, be it a method, a computed property, or even a data object. Keeping a functional code block in one place brings some other advantages, such as potentially reducing developer overhead and refactor complexity.

  • HelloWorldMixin.js
// Mixins are just JavaScript objects
const HelloWorldMixin = {
  computed: {
    helloWorld() {
      return 'Hello from Vue.js!';
    },
  },
};

export default HelloWorldMixin;
  • HelloWorld.vue
<template>
  <div>
    <!-- Hello from Vue.js! -->
    <p>{{ helloWorld }}</p>
  </div>
</template>

<script>
import HelloWorldMixin from 'path/to/HelloWorldMixin.js';

export default {
  mixins: [HelloWorldMixin]
};
</script>

Pure vs. Impure Mixins

There are a couple approaches to mixins. One option is to use them almost as a form of abstract class. For instance, you can create methods that require some property on the component but leave it up to the component to determine how that property is computed. The approach I tend to prefer, however, is creating a pure mixin, one where any required dependencies are kept within the mixin. This kind of encapsulation transforms a mixin into a plug-and-play piece of code. Keeping that code isolated can also be handy when writing tests. Properly testing a mixin often requires creating mock components, and a pure mixin’s detachment from the component means you can test without relying on component internals.

const ImpureMixin = {
  computed: {
    helloImpurity() {
      // There is no guarantee that the component will have this key
      return `I sure hope my component has ${this.importantProperty}`;
    },
  },
}l

const PureMixin = {
  data() {
    return {
      // Instead, include any expected keys within the mixin
      importantProperty: 'the required value!',
    };
  },
  computed: {
    helloPurity() {
      // Now we can expect our mixin to work in its default form
      return `I know my component will have ${this.importantProperty}`;
    },
  },
};

Mixin Override

Vue.js combines mixin options with the component options in the order they appear in the mixins array. While great for composing functionality, this can lead to one common oversight. Conflicting mixin values will be overwritten by whichever is further downstream. In addition, the component will win any conflicts with a mixin.

  • AllMixins.js
const FirstMixin = {
  computed: {
    hello() {
      return 'Hello from the first mixin';
    },
    goodbye() {
      return 'Goodbye from the first mixin';
    },
  },
};


const SecondMixin = {
  computed: {
    hello() {
      return 'Hello from the second mixin';
    },
  },
};

export default { FirstMixin, SecondMixin };
  • CollidingMixins.vue
<template>
  <div>
    <!-- Hello from the second mixin -->
    <p>{{ hello }}</p>
    <!-- Goodbye from the component -->
    <p>{{ goodbye }}</p>
  </div>
</template>

<script>
import { FirstMixin, SecondMixin } from 'path/to/AllMixins.js';

export default {
  // FirstMixin's `hello` property is overridden by `SecondMixin`
  mixins: [FirstMixin, SecondMixin],
  computed: {
    // Component takes precedence
    goodbye() {
      return 'Goodbye from the component';
    },
  },
};
</script>

Slots

While mixins are great at sharing behavior, they’re not well-suited for two other common features of reusable components: markup and styling. While mixins can technically include a template option, combining markup is finicky, so Vue.js will simply replace it on collision. That’s where slots come in. React developers may recognize the slot API as akin to the children props pattern, and practically, they’d be right. Components with a slot invert control back to the parent component for rendering children.

Below, the components with slots will be called providers, and the components using providers will be called consumers.

  • Cardify.vue (provider)
<template>
  <div class="card__container">
    <slot></slot>
  </div>
</template>
  • Using Cardify.vue (consumer)
<template>
  <cardify>
    <!-- Any child elements will be injected into the slot -->
    <p>Hello world!</p>
  </cardify>
</template>

Many reusable providers follow this pattern, housing specific logic and markup internally, but deferring to the consumer for what actually appears. One classic example is a generic card component that handles styling and some logic regarding selection, while leaving the actual content to the consumer. Vue.js slots come with a few other tools, such as support for default content, but two features in particular really expand their flexibility: named slots and scoped slots.

Named slots

Adding the name attribute to a slot grants some additional control for rendering the slot. The provider is able to modify the named slot without directly affecting the consumer’s implementation, while the consumer is able to specify which slots it wants to use. As an example, imagine you have a component that exposes a series of named slots for rendering inputs. The consumer component could take advantage of those names to control the order of those inputs in the source markup, avoiding a lot of in-line template logic and duplicated markup. That flexibility goes the other way as well, allowing the provider to change how they wrap specific slots without requiring change by the consumer. This specificity does come at a cost, however: the named slot will no longer blindly render children. Either the consumer must name their desired slots, or the provider must include a default slot.

  • CardifyWithName.vue
<template>
  <div class="card__container">
    <slot name="header"></slot>
    <slot name="content"></slot>
  </div>
</template>
  • Using CardifyWithName.vue
<template>
  <cardify-with-name>
    <!-- Name the slots to be used -->
    <p slot="header">Hello world!</p>
    <p slot="content">Check out my fancy named slots</p>
    <!-- Will not appear! -->
    <p>Where am I?</p>
  </cardify-with-name>
</template>

Scoped slots

Another pattern commonly used in React is the render props pattern. At first glance, slots have one significant downside when compared to JSX. Render functions frequently reference child properties, as the child is the one invoking the function, typically with its props and/or state. Templates, and by extension slots, are scoped at compilation, and trying to reference child properties in-line will result in failure. Vue.js does support JSX, but it also has a template-based solution in scoped slots, an equally flexible alternative.

  • CardifyWithScope.vue
<template>
  <div class="card__container">
    <!-- Give props to slots like any other component -->
    <slot :title="title"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return { title: 'Inside of CardifyWithScope!' };
  },
};
</script>
  • Using CardifyWithScope.vue
<template>
  <cardify-with-scope>
    <p>Hello world!</p>
    <!-- Give the slot scope a name -->
    <p slot-scope="insideComponent">
      <!-- All props given to the slot are now available -->
      {{ insideComponent.title }}
    </p>
  </cardify-with-scope>
</template>

By adding scope to a slot, we are able to create valid declarations in the consumer, while relying on the provider to hand it the actual data. Once the scope is declared, it can be referenced within the slot’s scope. To really drive home how closely this mirrors render props, slot scopes even support in-line destructuring (and defaulting, should you need).

  • Using CardifyWithScope.vue
<template>
  <cardify-with-scope>
    <p>Hello world!</p>
    <!-- Just like JSX -->
    <p slot-scope="{ title }">
      {{ title }}
    </p>
  </cardify-with-scope>
</template>

Summary

One of the greatest strengths of Vue.js is how much it works to reduce developer workload. React is awesome because of how generic its API is, but Vue.js leverages its more opinionated environment to provide you a simpler workflow from top to bottom. Single-file components, plugins, and a fantastic CLI provide seamless project initialization, while mixins and slots provide convenient APIs for modularizing your UI into flexible components. An ever growing number of web developers have been considering Vue.js, and many of us at Big Nerd Ranch are fans. If you’ve got any questions about Vue.js, or maybe you have even better ideas than the above, post a comment!

Steve Sparks

Reviewer Big Nerd Ranch

Steve Sparks has been a Nerd since 2011 and an electronics geek since age five. His primary focus is on iOS, watchOS, and tvOS development. He plays guitar and makes strange and terrifying contraptions. He lives in Atlanta with his family.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News