Rewriting a Vue 2.x component with Vue Composition API
Tags: vue, javascript
Published: 2019-12-29 11:19:07

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](https://github.com/vuejs/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:

```vuejs
<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>

```
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
```

In your `main.js` add the following two lines:
```javascript
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
```


# 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`:

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

export default {
  setup() {
    const changes = ref(0);
    return {
      changes,
    };
  },
};
```

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. 

```javascript
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),
    };
  },
}
```

# Move the computed properties
```javascript
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,
    };
  },
}
```

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

```javascript
const normalizeName = name => name.toUpperCase();
```

## 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.

```javascript
  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,
    };
  },
```
## `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

```vuejs
<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>
```
# Move the watch functions

```javascript
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);
    });
  },
};
```

# Define lifecycle hooks

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

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

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



References: 
[Vue Composition API RFC](https://vue-composition-api-rfc.netlify.com/)
[VueMastery Vue 3 Cheat sheet](https://www.vuemastery.com/vue-3-cheat-sheet/)
[GitHub Repo](https://github.com/andyghiuta/vue-composition-test)