loading...

Trying the composition API in Vue3

dasdaniel profile image Daniel Poda πŸ‡¨πŸ‡¦ ・5 min read

Trying the composition API in Vue3

On October 4th, The Vue-3 code was made public at github.com/vuejs/vue-next
It is still in pre-alpha monorepo that you can't quite use the same way as Vue2 just yet. There is also functionality from Vue2 that hasn't reached parity yet. At the time of writing, these are SSR, kep-alive, transitions, DOM-specific transforms (v-on, v-dom, etc.)

It does allow us, however, to start playing with though.

I spent the last couple nights trying to make some sample code to work. Unfortunately, the going was a bit tough. I wanted to start by making a parent component pass a prop to a child component. It took me a lot longer than I expected, but that was mostly my fault. I'll try to describe the path I took to get it eventually to work, and some of the mistakes I've made.

Get the repo

I started by downloading the git repo with the following commands

# clone the vue-next mono repo to vue-next folder
git clone https://github.com/vuejs/vue-next.git

cd vue-next

# install dependencies
npm install

# build vue.global.js
npm run dev

this will generate a vue.global.js file at packages\vue\dist\vue.global.js

Of course later I realized that the best place to start is here: https://github.com/vuejs/vue-next/blob/master/.github/contributing.md

The problem I had was that the file that was generated mounts Vue as a global, so is not suited for use with bundlers like parcel or webpack, which as what I was trying to use it with. In the contributing link from the repo, there are further instructions for generating other builds, but I've decided to use the global package instead with a simple file server (like serve or http-sever), it even works on online code editors like jsfiddle, which I ended up using.

I've found a sample code from the vue-composition-api-rfc at https://vue-composition-api-rfc.netlify.com/#basic-example

<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>

<script>
  import { reactive, computed } from "vue";

  export default {
    setup() {
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2)
      });

      function increment() {
        state.count++;
      }

      return {
        state,
        increment
      };
    }
  };
</script>

First I made it available by uploading it to gitlab as a gist and generating a rawgist link for includeing in a jsfiddle

https://gistcdn.githack.com/dasDaniel/f3cebc1274339055729c6e887ca8d2ad/raw/8f0432bfd812c453cdecee61862963fe3e24119a/vue.global.js

I had to make some changes to make it work with the global package, since that doesn't support Single File Components.

HTML:

<div id="app"></div>

<template id="appTemplate">
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>

JS:

const { reactive, computed } = Vue

const MyComponent = {
  setup(props) {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    });

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    };
  },
  template: document.getElementById("appTemplate").innerHTML
};

const app = Vue.createApp({})
app.mount(MyComponent, "#app")

As you can see, instead of using template literals or SFC, I used the <template> html tag and referenced it with getElementById. Otherwise it's pretty much the same.

The next goal was to add another component and pass a prop to it.

I added the following code to the appTemplate

<my-component :count="state.count"></my-component>

and this to the script


const MyComponent = Vue.createComponent({
  template: `<div>my component {{count}}<div>`,
  props: {
    count: Number
  },
  setup(props){
    return {...props}
  }
})

I also registered the component before mounting the app

app.component('my-component', MyComponent)

The result was that the prop was passed initially with a value of 0, and then it didn't update any after that. So I tried copying what the app does.

const MyComponent = Vue.createComponent({
  template: `<div>my component {{state.count}}<div>`,
  props: {
    count: Number
  },
  setup(props){
    const state = reactive({...props})
    return {
        state
    }
  }
});

Shis still doesn't work and it's not clear to me why.

Now is the time when I frantically try a hundred different things and nothing seems to work. I could list all the things, but I'll just mention a few.

// added `onUpdated`
const { reactive, computed, onUpdated } = Vue;

const MyComponent = Vue.createComponent({
  template: `<div>my component {{state.count}}<div>`,
  props: {
    count: Number
  },
  setup(props){
    const state = reactive({...props})
    // added onUpdated function
    onUpdated(() => {
        state.count = props.count
    })
    return {
        state
    }
  }
});

When I console logged the state after the update, the state did change, but the template did not update. This wasn't making any sense.

Eventually after more reading and debugging I've found out two things.

The way to do it correctly from what I gather is to use reactive AND computed

  const state = reactive({
    count: computed(() => props.count)
  })

The other thing that I eventually noticed, was that my div tag wasn't closed. This was causing the layout to only render the first time, and likely why I may have tried something that should have been working (like using onUpdate) and weren't.

the working code (https://jsfiddle.net/dapo/fp34wj6z/)

<div id="app"></div>

<template id="appTemplate">
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
  <pre>{{ state }}</pre>
  <my-component :count="state.count"></my-component>
</template>

<template id="myComponent">
  <div>
    Child
    <pre>{{ state }}</pre>
  </div>
</template>
const {
  reactive,
  computed,
} = Vue;

const MyComponent = Vue.createComponent({
  props: {
    count: Number
  },
  setup(props) {
    const state = reactive({
      count: computed(() => props.count),
      triple: computed(() => props.count * 3)
    });

    return {
      state,
    }
  },
  template: document.getElementById('myComponent').innerHTML
});

const App = {
  setup(props) {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    });

    function increment() {
      state.count++;
    }

    return {
      state,
      increment
    };
  },
  template: document.getElementById('appTemplate').innerHTML
};

const app = Vue.createApp({});
app.component('my-component', MyComponent)
app.mount(App, "#app");

TL;DR;

  • I didn't watch my template syntax and missed that a tag wasn't closed, which was causing the rendering not to work properly.
  • should have spent more time reading the docs than trying madly to just get it to work.

Resources :

Anyway, I'll be playing more with this in the future. I certainly subscribe to the benefits of the composition API, I just need to spend some more time understanding the how ref, reactive, computed, and watch all work together.

Discussion

markdown guide
 

Currently v-model is not supported. So I've used a react-useState-style helper function to have reusable functionality (somewhat) similar to v-model.

jsfiddle.net/dapo/a1fsw23n/

It's amazing what RTFMing can do for your understanding.