DEV Community

Andy G
Andy G

Posted on

Rewriting a Vue 2.x component with Vue Composition API

Vue 3 will come with an additional advanced API called "Composition", which will be "a set of additive, function-based APIs that allow flexible composition of component logic."

To experiment with it and provide feedback, we can already use with Vue 2.x the @vue/composition-api plugin.

Below is a walk-through of moving from using the "standard" Vue API to the Composition API.

The component I'm going to rewrite is the following:

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <label>Enter your name: </label>
    <input type="text" v-model="name" /><br>
    <label>Set your age: </label>
    <button type="button" @click="decreaseAge"> - </button>
    <span> {{age}} </span>
    <button type="button" @click="increaseAge"> + </button>
    <p><small>You made {{changes}} changes to your info</small></p>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String,
    value: String,
    autoFocus: Boolean,
    select: Boolean,
  },
  data() {
    const info = this.splitInfo(this.value);
    return {
      ...info,
      changes: 0,
    };
  },
  computed: {
    personInfo() {
      return `${this.normalizeName(this.name)}-${this.age}`;
    },
  },
  watch: {
    value(outsideValue) {
      Object.assign(this, this.splitInfo(outsideValue));
    },
    personInfo() {
      this.changes += 1;
      this.$emit('input', this.personInfo);
    },
    autoFocus() {
      this.setFocus();
    },
    select() {
      this.setSelect();
    },
  },
  mounted() {
    this.setFocus();
    this.setSelect();
  },
  methods: {
    setFocus() {
      if (this.autoFocus) {
        this.$el.querySelector('input').focus();
      }
    },
    setSelect() {
      if (this.select) {
        this.$el.querySelector('input').select();
      }
    },
    normalizeName(name) {
      return name.toUpperCase();
    },
    increaseAge() {
      this.age += 1;
    },
    decreaseAge() {
      this.age -= 1;
    },
    splitInfo(info) {
      const [name, age] = info.split('-');
      return { name, age: parseInt(age, 10) };
    },
    setChanges() {
      this.changes += 1;
    },
  },
};
</script>

Enter fullscreen mode Exit fullscreen mode

It's a "hello world" of the Vue components, accepting a v-model and a few other props. It emits an input event, changing the v-model.

Installation and setup

Install composition api:

$ npm i @vue/composition-api --save
Enter fullscreen mode Exit fullscreen mode

In your main.js add the following two lines:

import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
Enter fullscreen mode Exit fullscreen mode

Start with an empty setup

Add an empty setup function to the component. It is called before the beforeCreate hook and does not have access to component instance (this), but the properties returned from it will be exposed in the instance.
This function will be called with two parameters: props and context. The former being pretty self explanatory, while the latter being an object which exposes a selective list of properties that were previously exposed on this in 2.x APIs, among which the most important are: parent, refs, attrs, emit, slots.

Move data to reactive/refs

The model that is defined in data can now be defined with one of the functions reactive or ref, depending on the use case. The first takes an object and returns a reactive proxy of it while the second takes a value and returns a reactive mutable object with a single value property.

Moving the changes from data to setup:

import { ref } from '@vue/composition-api';

export default {
  setup() {
    const changes = ref(0);
    return {
      changes,
    };
  },
};
Enter fullscreen mode Exit fullscreen mode

For the other two properties name and age, which are extracted from the value prop, you need to take into consideration that we have no access to this in setup, hence value needs to be taken from props parameter and splitInfo can to be defined outside the component info since it doesn't use the instance anyway.

import { ref, reactive, toRefs } from '@vue/composition-api';

const splitInfo = (info) => {
  const [name, age] = info.split('-');
  return { name, age: parseInt(age, 10) };
};

export default {
  setup(props) {
    // reactive properties
    const changes = ref(0);
    const info = reactive(splitInfo(props.value));
    // return the state with the reactive properties & methods
    // each property must be a ref
    return {
      // return properties
      // changes is a ref, can be returned as such
      changes,
      // to convert a reactive object to a plain object with refs, use toRefs
      ...toRefs(info),
    };
  },
}
Enter fullscreen mode Exit fullscreen mode

Move the computed properties

import { ref, reactive, toRefs, computed } from '@vue/composition-api';

export default {
  setup(props) {
    // reactive properties
    const changes = ref(0);
    const info = reactive(splitInfo(props.value));

    // computed properties
    const personInfo = computed(() => `${normalizeName(info.name)}-${info.age}`);

    // return the state with the reactive properties & methods
    // each property must be a ref
    return {
      // return properties
      // changes is a ref, can be returned as such
      changes,
      // to convert a reactive object to a plain object with refs, use toRefs
      ...toRefs(info),
      // return computed properties
      personInfo,
    };
  },
}
Enter fullscreen mode Exit fullscreen mode

Move the methods

Declare those that don't use the instance outside of the component declaration

const normalizeName = name => name.toUpperCase();
Enter fullscreen mode Exit fullscreen mode

Declare those that use the state inside the setup

In order to have access to the reactive properties, methods that use them, need to be defined in the same scope.

  setup(props) {
    // reactive properties
    // ...
    // computed properties
    // ...
    // methods
    const increaseAge = () => {
      info.age += 1;
    };
    const decreaseAge = () => {
      info.age -= 1;
    };
    const setChanges = () => {
      // refs need to be accessed with the value property
      changes.value += 1;
    };
    // return the state with the reactive properties & methods
    // each property must be a ref
    return {
      // return properties
      // ...
      // return computed properties
      // ...
      // return methods
      increaseAge,
      decreaseAge,
      setChanges,
    };
  },
Enter fullscreen mode Exit fullscreen mode

this.$el needs to be handled differently

Again, having no instance, we don't have this.$el, but we do have refs on the context object passed to setup. Hence we can add a ref attribute to the root node of the component and use that

<template>
  <div ref="el" />
</template>

<script>
export default {
  setup(props, context) {
    // reactive properties
    // ...
    // computed properties
    // ...
    // methods
    // ...
    const setFocus = () => {
      if (props.autoFocus) {
        context.refs.el.querySelector('input').focus();
      }
    };
    const setSelect = () => {
      if (props.select) {
        context.refs.el.querySelector('input').select();
      }
    };

  },
};
</script>
Enter fullscreen mode Exit fullscreen mode

Move the watch functions

import { 
  ref, reactive, toRefs, computed, watch, onMounted,
} from '@vue/composition-api';

export default {
  setup(props, context) {
    // reactive properties
    // ...
    // computed properties
    // ...
    // methods
    // ...
    // define watches
    // props, refs and reactive objects can be watched for changes
    // can watch a getter function
    watch(() => props.autoFocus, setFocus);
    watch(() => props.select, setSelect);
    // optionally, can have be lazy (won't run on component initialize)
    // defaults to false, contrary to how watches work in Vue 2
    watch(() => props.value, (outsideValue) => {
      Object.assign(info, splitInfo(outsideValue));
    }, { lazy: true });
    // watch a specific ref (computed)
    watch(personInfo, () => {
      setChanges();
      context.emit('input', personInfo.value);
    });
  },
};
Enter fullscreen mode Exit fullscreen mode

Define lifecycle hooks

In this case, mounted becomes onMounted which is called in the setup.

import { 
  ref, reactive, toRefs, computed, watch, onMounted,
} from '@vue/composition-api';

export default {
  setup(props, context) {
    // ...
    // lifecycle hooks
    onMounted(() => {
      setFocus();
      setSelect();
    });
    // ...
  },
};
Enter fullscreen mode Exit fullscreen mode

References:
Vue Composition API RFC
VueMastery Vue 3 Cheat sheet
GitHub Repo

Top comments (0)