Skip to content
Jon Carifio edited this page Apr 23, 2024 · 3 revisions

Options -> Composition API migration guide

Up until now, we've written our components using Vue's Options API, but PR #6 changes our the toolkit to use the Vue's newer Composition API. While there's nothing inherently wrong with the Options API, I think the Composition API is better suited to our use case and will let use reuse more code across components. The ability to refactor reactive logic out of components and into composables should help us keep the components themselves simpler, and I think we'll gain a lot of flexibility by not needing to worry about inheritance. Note that this change only really affects the JavaScript portion of the component; creating the template and CSS should be unchanged.

Component scope

The most immediate difference between the Options and Composition APIs is how the top-level component scope is handled. In the Options API, we export an object (wrapped with defineComponent). That object serves as the overall "scope" of the component - all of our reactive data, computed values, watchers, lifecycle hooks, etc. are attached to that object, and any data that we want to pass into the template has to come from it. In the Composition API, there is no component "object" - instead, we do all of our component setup inside of a special "setup" script tag, which is indicated in the template as <script setup> (we can add other attributes as usual, e.g. <script setup lang="ts">). All of our reactive stuff - data, watchers, hooks, etc. - are defined as top-level elements within the setup tag. A reactive item can then reference another one directly (no component this required).

Reactive data

The Composition API has a few main pieces of reactive state. The relevant documentation is here, but I'll talk here about how these relate to what we've previously used:

Component data

In the Options API, we defined a data() method on the component, which returned an object whose members defined the reactive data for the component. In the Composition API we define our base reactive data using the ref and reactive elements.

  • ref should be used for primitives, or for objects where you expect that you'll be wanting to reassign the entire object at once
  • reactive should be used on objects want "deep" reactivity - you'll be changing the object's member values and want to react to these updates
    • Note that with reactive, you CANNOT replace the entire object (e.g. don't do myReactive = someNewValue) or else reactivity will be broken, since Vue looks at property access and we've now changed the object reference
    • You also CANNOT destructure and keep reactivity (e.g. if you do const { someMember } = myReactive, then someMember will not be reactive)

Vue recommends using ref as the default for declaring reactive state and I would agree, but reactive is there for when the use case is right.

As an overall example, if we previously had

data() {
  return {
    flag: false,
    counter: 0,
    someObject: { a: 0, b: 'text' },
  };
}

with Composition API, we would write the following inside our <script setup>:

const flag = ref(false);
const counter = ref(0);
const someObject = reactive({ a: 0, b: 'text' });  // or maybe ref, depends on the use case 

To modify a ref in JavaScript, you assign to the value property. That is, to update the counter, we would write counter.value += 1. For a reactive, just update the member value as normal (someObject.a = 2). However, inside of the template you don't need the .value for a ref - that is, you can just do counter += 1.

Computed properties

In the Options API, we defined computed properties as functions inside of the component object's computed member that returned a value. With Composition API, we can just use the computed function of the Reactivity API. That is, instead of

...
computed: {
  date() {
    return new Date(this.timestamp);
  },
  ...<rest of computeds>...
}
...

we would now write (inside of our <script setup>)

const date = computed(() => new Date(timestamp.value);

Watchers

Similar to computed properties, we now define watchers using a builtin function watch. The first argument of watch is the reactive item that we want to watch, the second argument is the code to execute on a change, and the third (optional) argument is for setting options. So,

watch: {
  counter(newCount: number, oldCount: number) {
    // Do some counter update stuff
  }
}

now becomes

watch(count, (newCount: number, oldCount: number) => {
  // Do some counter update stuff
});

The watch options parameters allows specifying some details of when the watcher runs, including whether we want to wait for the DOM to update before calling the watcher (very cool!) There are also now a few different flavors of watcher, including ones that allow multiple dependencies; see the details here.

Lifecycle hooks

Just like computed properties and watchers, lifecycle hooks are also now handled via builtin functions. In the Composition API, the lifecycle hooks have changed a bit. The full list is here. The most important hook for us, the mounted hook, still exists via onMounted. The created hook no longer exists; I would imagine that in general we will want to use onBeforeMount instead. There are some restrictions on which hooks are run in a server-side rendering (SSR) environment, but we aren't doing that, so we can ignore that for now.

So if we had previously written

mounted() {
 // Do some stuff
}

inside of our component, we would now write

onMounted(() => {
  // Do some stuff
});

inside of our script setup tag.

Composables

This is the "new" piece of the Composition API, and I think the feature that makes this change the most appealing for us. Basically, a composable is a function that allows us to encapsulate and reuse stateful logic (that will generally be reactive). Vue's documentation example shows this off well. A composable can return to the caller some piece(s) of reactive state that have already been set up to respond to relevant events the correct way - i.e. in the given example, the returned coordinate refs are already set up to listen for mouse movement on the page. Additionally, composables can use the owning component's lifecycle hooks to do setup/cleanup. Thus we get all the advantages of a mixin-style approach, without having to worry about inheritance. If we don't want some particular composable for a story, we just don't use it, without having to worry about whether some base component was using this functionality or having to do anything to our component's lifecycle hooks. And if we do want it, we just import the composable and use the returned reactive state in our component, same as we would regular component data.

Vue DS Template

The Vue DS template has already been converted to use the Composition API. It's a simple component but I think can be a good reference of how to do things in the Composition API style. Note that the template and style pieces of that component didn't need to change at all - only the JavaScript needed to be adjusted.

Clone this wiki locally